mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
51198744ff
## Summary - add a `--port` flag to `scripts/develop.sh` so the API can run on a non-default port - make the script use the selected API port for access URL defaults, readiness checks, login, proxy wiring, and printed URLs - reject oversized `--port` values early and treat an existing dev frontend on port 8080 as a conflict when the requested API is not already running - pin the frontend dev server to port 8080 so inherited `PORT` environment variables do not move it to a different port ## Testing - `bash -n scripts/develop.sh` - `shellcheck -x scripts/develop.sh` - `bash scripts/develop.sh --port abc` - `bash scripts/develop.sh --port 8080` - `bash scripts/develop.sh --port 999999999999999999999` - started `./scripts/develop.sh --port 3001` and verified: - `http://127.0.0.1:3001/healthz` - `http://127.0.0.1:3001/api/v2/buildinfo` - `http://127.0.0.1:8080/healthz` - `http://127.0.0.1:8080/api/v2/buildinfo` - simulated an existing dev frontend on `127.0.0.1:8080` and verified `./scripts/develop.sh --port 3001` exits with a conflict error
356 lines
13 KiB
Bash
Executable File
356 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Usage: ./develop.sh [--agpl] [--port <port>]
|
|
#
|
|
# If the --agpl parameter is specified, builds only the AGPL-licensed code (no
|
|
# Coder enterprise features). The --port parameter changes the API port. The
|
|
# frontend dev server still listens on port 8080.
|
|
|
|
SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
|
|
# shellcheck source=scripts/lib.sh
|
|
source "${SCRIPT_DIR}/lib.sh"
|
|
|
|
# Allow toggling verbose output
|
|
[[ -n ${VERBOSE:-} ]] && set -x
|
|
set -euo pipefail
|
|
|
|
api_port=3000
|
|
web_port=8080
|
|
proxy_port=3010
|
|
CODER_DEV_ACCESS_URL="${CODER_DEV_ACCESS_URL:-}"
|
|
access_url_set=0
|
|
if [ -n "${CODER_DEV_ACCESS_URL}" ]; then
|
|
access_url_set=1
|
|
fi
|
|
DEVELOP_IN_CODER="${DEVELOP_IN_CODER:-0}"
|
|
debug=0
|
|
DEFAULT_PASSWORD="SomeSecurePassword!"
|
|
password="${CODER_DEV_ADMIN_PASSWORD:-${DEFAULT_PASSWORD}}"
|
|
use_proxy=0
|
|
multi_org=0
|
|
|
|
# Ensure that extant environment variables do not override
|
|
# the config dir we use to override auth for dev.coder.com.
|
|
unset CODER_SESSION_TOKEN
|
|
unset CODER_URL
|
|
|
|
args="$(getopt -o "" -l access-url:,use-proxy,agpl,debug,password:,multi-organization,port: -- "$@")"
|
|
eval set -- "$args"
|
|
while true; do
|
|
case "$1" in
|
|
--access-url)
|
|
CODER_DEV_ACCESS_URL="$2"
|
|
access_url_set=1
|
|
shift 2
|
|
;;
|
|
--port)
|
|
api_port="$2"
|
|
shift 2
|
|
;;
|
|
--agpl)
|
|
export CODER_BUILD_AGPL=1
|
|
shift
|
|
;;
|
|
--password)
|
|
password="$2"
|
|
shift 2
|
|
;;
|
|
--use-proxy)
|
|
use_proxy=1
|
|
shift
|
|
;;
|
|
--multi-organization)
|
|
multi_org=1
|
|
shift
|
|
;;
|
|
--debug)
|
|
debug=1
|
|
shift
|
|
;;
|
|
--)
|
|
shift
|
|
break
|
|
;;
|
|
*)
|
|
error "Unrecognized option: $1"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [ "${CODER_BUILD_AGPL:-0}" -gt "0" ] && [ "${use_proxy}" -gt "0" ]; then
|
|
echo '== ERROR: cannot use both external proxies and APGL build.' && exit 1
|
|
fi
|
|
|
|
if [ "${CODER_BUILD_AGPL:-0}" -gt "0" ] && [ "${multi_org}" -gt "0" ]; then
|
|
echo '== ERROR: cannot use both multi-organizations and APGL build.' && exit 1
|
|
fi
|
|
|
|
validate_port() {
|
|
local port=$1
|
|
local flag=$2
|
|
|
|
if ! [[ "${port}" =~ ^[0-9]+$ ]]; then
|
|
error "${flag} must be an integer between 1 and 65535"
|
|
fi
|
|
if [ "${#port}" -gt 5 ]; then
|
|
error "${flag} must be an integer between 1 and 65535"
|
|
fi
|
|
if ((10#${port} < 1 || 10#${port} > 65535)); then
|
|
error "${flag} must be an integer between 1 and 65535"
|
|
fi
|
|
}
|
|
|
|
validate_port "${api_port}" "--port"
|
|
if [ "${api_port}" -eq "${web_port}" ]; then
|
|
error "--port cannot use ${web_port} because the frontend dev server uses that port"
|
|
fi
|
|
if [ "${use_proxy}" -gt "0" ] && [ "${api_port}" -eq "${proxy_port}" ]; then
|
|
error "--port cannot use ${proxy_port} when --use-proxy is enabled because the workspace proxy uses that port"
|
|
fi
|
|
if [ "${access_url_set}" -eq 0 ]; then
|
|
CODER_DEV_ACCESS_URL="http://127.0.0.1:${api_port}"
|
|
fi
|
|
|
|
api_url="http://127.0.0.1:${api_port}"
|
|
api_local_url="http://localhost:${api_port}"
|
|
web_url="http://127.0.0.1:${web_port}"
|
|
|
|
if [ -n "${CODER_AGENT_URL:-}" ]; then
|
|
DEVELOP_IN_CODER=1
|
|
fi
|
|
|
|
# Preflight checks: ensure we have our required dependencies, and make sure
|
|
# nothing is listening on the configured API or frontend ports.
|
|
dependencies curl git go jq make pnpm
|
|
|
|
if curl --silent --fail "${api_url}" >/dev/null 2>&1; then
|
|
# Check if this is the Coder development server.
|
|
if curl --silent --fail "${api_url}/api/v2/buildinfo" 2>&1 | jq -r '.version' >/dev/null 2>&1; then
|
|
echo "== INFO: Coder development server is already running on port ${api_port}!" && exit 0
|
|
else
|
|
echo "== ERROR: something is listening on port ${api_port}. Kill it and re-run this script." && exit 1
|
|
fi
|
|
fi
|
|
|
|
if curl --fail "${web_url}" >/dev/null 2>&1; then
|
|
# Check if this is the Coder development frontend.
|
|
if curl --silent --fail "${web_url}/api/v2/buildinfo" 2>&1 | jq -r '.version' >/dev/null 2>&1; then
|
|
echo "== ERROR: Coder development frontend is already running on port ${web_port}, but the requested API on port ${api_port} is not already running. Stop the frontend and re-run this script." && exit 1
|
|
else
|
|
echo "== ERROR: something is listening on port ${web_port}. Kill it and re-run this script." && exit 1
|
|
fi
|
|
fi
|
|
|
|
# Compile the CLI binary. This should also compile the frontend and refresh
|
|
# node_modules if necessary.
|
|
GOOS="$(go env GOOS)"
|
|
GOARCH="$(go env GOARCH)"
|
|
DEVELOP_IN_CODER="${DEVELOP_IN_CODER}" make -j "build/coder_${GOOS}_${GOARCH}"
|
|
|
|
# Use the coder dev shim so we don't overwrite the user's existing Coder config.
|
|
CODER_DEV_SHIM="${PROJECT_ROOT}/scripts/coder-dev.sh"
|
|
|
|
# Stores the pid of the subshell that runs our main routine.
|
|
ppid=0
|
|
# Tracks pids of commands we've started.
|
|
pids=()
|
|
exit_cleanup() {
|
|
set +e
|
|
# Set empty interrupt handler so cleanup isn't interrupted.
|
|
trap '' INT TERM
|
|
# Remove exit trap to avoid infinite loop.
|
|
trap - EXIT
|
|
|
|
# Send interrupts to the processes we started. Note that we do not
|
|
# (yet) want to send a kill signal to the entire process group as
|
|
# this can halt processes started by graceful shutdown.
|
|
kill -INT "${pids[@]}" >/dev/null 2>&1
|
|
# Use the hammer if things take too long.
|
|
{ sleep 5 && kill -TERM "${pids[@]}" >/dev/null 2>&1; } &
|
|
|
|
# Wait for all children to exit (this can be aborted by hammer).
|
|
wait_cmds
|
|
|
|
# Just in case, send termination to the entire process group
|
|
# in case the children left something behind.
|
|
kill -TERM -"${ppid}" >/dev/null 2>&1
|
|
|
|
exit 1
|
|
}
|
|
start_cmd() {
|
|
name=$1
|
|
prefix=$2
|
|
shift 2
|
|
|
|
echo "== CMD: $*" >&2
|
|
|
|
FORCE_COLOR=1 "$@" > >(
|
|
# Ignore interrupt, read will keep reading until stdin is gone.
|
|
trap '' INT
|
|
|
|
while read -r line; do
|
|
if [[ $prefix == date ]]; then
|
|
echo "[$name] $(date '+%Y-%m-%d %H:%M:%S') $line"
|
|
else
|
|
echo "[$name] $line"
|
|
fi
|
|
done
|
|
echo "== CMD EXIT: $*" >&2
|
|
# Let parent know the command exited.
|
|
kill -INT $ppid >/dev/null 2>&1
|
|
) 2>&1 &
|
|
pids+=("$!")
|
|
}
|
|
wait_cmds() {
|
|
wait "${pids[@]}" >/dev/null 2>&1
|
|
}
|
|
fatal() {
|
|
echo "== FAIL: $*" >&2
|
|
kill -INT $ppid >/dev/null 2>&1
|
|
}
|
|
|
|
# This is a way to run multiple processes in parallel, and have Ctrl-C work correctly
|
|
# to kill both at the same time. For more details, see:
|
|
# https://stackoverflow.com/questions/3004811/how-do-you-run-multiple-programs-in-parallel-from-a-bash-script
|
|
(
|
|
ppid=$BASHPID
|
|
# If something goes wrong, just bail and tear everything down
|
|
# rather than leaving things in an inconsistent state.
|
|
trap 'exit_cleanup' INT TERM EXIT
|
|
trap 'fatal "Script encountered an error"' ERR
|
|
|
|
cdroot
|
|
DEBUG_DELVE="${debug}" DEVELOP_IN_CODER="${DEVELOP_IN_CODER}" start_cmd API "" "${CODER_DEV_SHIM}" server --http-address "0.0.0.0:${api_port}" --swagger-enable --access-url "${CODER_DEV_ACCESS_URL}" --dangerous-allow-cors-requests=true --enable-terraform-debug-mode "$@"
|
|
|
|
echo '== Waiting for Coder to become ready'
|
|
# Start the timeout in the background so interrupting this script
|
|
# doesn't hang for 60s.
|
|
timeout 60s bash -c "until curl -s --fail ${api_local_url}/healthz > /dev/null 2>&1; do sleep 0.5; done" ||
|
|
fatal 'Coder did not become ready in time' &
|
|
wait $!
|
|
|
|
# Check if credentials are already set up to avoid setting up again.
|
|
"${CODER_DEV_SHIM}" list >/dev/null 2>&1 && touch "${PROJECT_ROOT}/.coderv2/developsh-did-first-setup"
|
|
|
|
if ! "${CODER_DEV_SHIM}" whoami >/dev/null 2>&1; then
|
|
# Try to create the initial admin user.
|
|
echo "Login required; use admin@coder.com and password '${password}'" >&2
|
|
|
|
if "${CODER_DEV_SHIM}" login "${api_url}" --first-user-username=admin --first-user-email=admin@coder.com --first-user-password="${password}" --first-user-full-name="Admin User" --first-user-trial=false; then
|
|
# Only create this file if an admin user was successfully
|
|
# created, otherwise we won't retry on a later attempt.
|
|
touch "${PROJECT_ROOT}/.coderv2/developsh-did-first-setup"
|
|
else
|
|
echo 'Failed to create admin user. To troubleshoot, try running this command manually.'
|
|
fi
|
|
|
|
# Try to create a regular user.
|
|
"${CODER_DEV_SHIM}" users create --email=member@coder.com --username=member --full-name "Regular User" --password="${password}" ||
|
|
echo 'Failed to create regular user. To troubleshoot, try running this command manually.'
|
|
fi
|
|
|
|
# Create a new organization and add the member user to it.
|
|
if [ "${multi_org}" -gt "0" ]; then
|
|
another_org="second-organization"
|
|
if ! "${CODER_DEV_SHIM}" organizations show selected --org "${another_org}" >/dev/null 2>&1; then
|
|
echo "Creating organization '${another_org}'..."
|
|
(
|
|
"${CODER_DEV_SHIM}" organizations create -y "${another_org}"
|
|
) || echo "Failed to create organization '${another_org}'"
|
|
fi
|
|
|
|
if ! "${CODER_DEV_SHIM}" org members list --org ${another_org} | grep "^member" >/dev/null 2>&1; then
|
|
echo "Adding member user to organization '${another_org}'..."
|
|
(
|
|
"${CODER_DEV_SHIM}" organizations members add member --org "${another_org}"
|
|
) || echo "Failed to add member user to organization '${another_org}'"
|
|
fi
|
|
|
|
echo "Starting external provisioner for '${another_org}'..."
|
|
(
|
|
start_cmd EXT_PROVISIONER "" "${CODER_DEV_SHIM}" provisionerd start --tag "scope=organization" --name second-org-daemon --org "${another_org}"
|
|
) || echo "Failed to start external provisioner. No external provisioner started."
|
|
fi
|
|
|
|
# If we have docker available and the "docker" template doesn't already
|
|
# exist, then let's try to create a template!
|
|
template_name="docker"
|
|
# Determine the name of the default org with some jq hacks!
|
|
first_org_name=$("${CODER_DEV_SHIM}" organizations show me -o json | jq -r '.[] | select(.is_default) | .name')
|
|
if docker info >/dev/null 2>&1 && ! "${CODER_DEV_SHIM}" templates versions list "${template_name}" >/dev/null 2>&1; then
|
|
# sometimes terraform isn't installed yet when we go to create the
|
|
# template
|
|
echo "Waiting for terraform to be installed..."
|
|
sleep 5
|
|
|
|
echo "Initializing docker template..."
|
|
temp_template_dir="$(mktemp -d)"
|
|
"${CODER_DEV_SHIM}" templates init --id "${template_name}" "${temp_template_dir}"
|
|
# Run terraform init so we get a terraform.lock.hcl
|
|
pushd "${temp_template_dir}" && terraform init && popd
|
|
|
|
DOCKER_HOST="$(docker context inspect --format '{{ .Endpoints.docker.Host }}')"
|
|
printf 'docker_arch: "%s"\ndocker_host: "%s"\n' "${GOARCH}" "${DOCKER_HOST}" >"${temp_template_dir}/params.yaml"
|
|
(
|
|
echo "Pushing docker template to '${first_org_name}'..."
|
|
"${CODER_DEV_SHIM}" templates push "${template_name}" --directory "${temp_template_dir}" --variables-file "${temp_template_dir}/params.yaml" --yes --org "${first_org_name}"
|
|
if [ "${multi_org}" -gt "0" ]; then
|
|
echo "Pushing docker template to '${another_org}'..."
|
|
"${CODER_DEV_SHIM}" templates push "${template_name}" --directory "${temp_template_dir}" --variables-file "${temp_template_dir}/params.yaml" --yes --org "${another_org}"
|
|
fi
|
|
rm -rfv "${temp_template_dir}" # Only delete template dir if template creation succeeds
|
|
) || echo "Failed to create a template. The template files are in ${temp_template_dir}"
|
|
fi
|
|
|
|
if [ "${use_proxy}" -gt "0" ]; then
|
|
log "Using external workspace proxy"
|
|
(
|
|
# Attempt to delete the proxy first, in case it already exists.
|
|
"${CODER_DEV_SHIM}" wsproxy delete local-proxy --yes || true
|
|
# Create the proxy
|
|
proxy_session_token=$("${CODER_DEV_SHIM}" wsproxy create --name=local-proxy --display-name="Local Proxy" --icon="/emojis/1f4bb.png" --only-token)
|
|
# Start the proxy
|
|
start_cmd PROXY "" "${CODER_DEV_SHIM}" wsproxy server --dangerous-allow-cors-requests=true --http-address="localhost:${proxy_port}" --proxy-session-token="${proxy_session_token}" --primary-access-url="${api_local_url}"
|
|
) || echo "Failed to create workspace proxy. No workspace proxy created."
|
|
fi
|
|
|
|
# Start the frontend once we have a template up and running. We pin the
|
|
# port because some environments export PORT for unrelated services.
|
|
PORT="${web_port}" CODER_HOST="${api_url}" start_cmd SITE date pnpm --dir ./site dev --host
|
|
|
|
interfaces=(localhost)
|
|
if command -v ip >/dev/null; then
|
|
# shellcheck disable=SC2207
|
|
interfaces+=($(ip a | awk '/inet / {print $2}' | cut -d/ -f1))
|
|
elif command -v ifconfig >/dev/null; then
|
|
# shellcheck disable=SC2207
|
|
interfaces+=($(ifconfig | awk '/inet / {print $2}'))
|
|
fi
|
|
|
|
# Space padding used after the URLs to align "==".
|
|
space_padding=26
|
|
log
|
|
log "===================================================================="
|
|
log "== =="
|
|
log "== Coder is now running in development mode. =="
|
|
for iface in "${interfaces[@]}"; do
|
|
log "$(printf "== API: http://%s:${api_port}%$((space_padding - ${#iface}))s==" "$iface" "")"
|
|
done
|
|
for iface in "${interfaces[@]}"; do
|
|
log "$(printf "== Web UI: http://%s:${web_port}%$((space_padding - ${#iface}))s==" "$iface" "")"
|
|
done
|
|
if [ "${use_proxy}" -gt "0" ]; then
|
|
for iface in "${interfaces[@]}"; do
|
|
log "$(printf "== Proxy: http://%s:${proxy_port}%$((space_padding - ${#iface}))s==" "$iface" "")"
|
|
done
|
|
fi
|
|
log "== =="
|
|
log "== Use ./scripts/coder-dev.sh to talk to this instance! =="
|
|
log "$(printf "== alias cdr=%s/scripts/coder-dev.sh%$((space_padding - ${#PWD}))s==" "$PWD" "")"
|
|
log "===================================================================="
|
|
log
|
|
|
|
# Wait for both frontend and backend to exit.
|
|
wait_cmds
|
|
)
|