feat: bundle a local version of install.sh (#16064)

This commit is contained in:
ケイラ
2025-01-14 11:30:39 -07:00
committed by GitHub
parent 5a89e89d7e
commit a450121e74
4 changed files with 493 additions and 6 deletions

View File

@ -399,7 +399,17 @@ site/node_modules/.installed: site/package.json
cd site/
../scripts/pnpm_install.sh
site/out/index.html: site/node_modules/.installed $(shell find ./site $(FIND_EXCLUSIONS) -type f \( -name '*.ts' -o -name '*.tsx' \))
SITE_GEN_FILES := \
site/src/api/typesGenerated.ts \
site/src/api/rbacresourcesGenerated.ts \
site/src/api/countriesGenerated.ts \
site/src/theme/icons.json
site/out/index.html: \
site/node_modules/.installed \
site/static/install.sh \
$(SITE_GEN_FILES) \
$(shell find ./site $(FIND_EXCLUSIONS) -type f \( -name '*.ts' -o -name '*.tsx' \))
cd site/
# prevents this directory from getting to big, and causing "too much data" errors
rm -rf out/assets/
@ -541,22 +551,21 @@ GEN_FILES := \
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
vpn/vpn.pb.go \
site/src/api/typesGenerated.ts \
$(DB_GEN_FILES) \
$(SITE_GEN_FILES) \
coderd/rbac/object_gen.go \
codersdk/rbacresources_gen.go \
site/src/api/rbacresourcesGenerated.ts \
site/src/api/countriesGenerated.ts \
docs/admin/integrations/prometheus.md \
docs/reference/cli/index.md \
docs/admin/security/audit-logs.md \
coderd/apidoc/swagger.json \
provisioner/terraform/testdata/version \
site/e2e/provisionerGenerated.ts \
site/src/theme/icons.json \
examples/examples.gen.json \
$(TAILNETTEST_MOCKS) \
coderd/database/pubsub/psmock/psmock.go
# all gen targets should be added here and to gen/mark-fresh
gen: gen/db $(GEN_FILES)
.PHONY: gen

View File

@ -160,6 +160,11 @@ func New(opts *Options) *Handler {
handler.buildInfoJSON = html.EscapeString(string(buildInfoResponse))
handler.handler = mux.ServeHTTP
handler.installScript, err = parseInstallScript(opts.SiteFS, opts.BuildInfo)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "install.sh will be unavailable: %v", err.Error())
}
return handler
}
@ -169,8 +174,8 @@ type Handler struct {
secureHeaders *secure.Secure
handler http.HandlerFunc
htmlTemplates *template.Template
buildInfoJSON string
installScript []byte
// RegionsFetcher will attempt to fetch the more detailed WorkspaceProxy data, but will fall back to the
// regions if the user does not have the correct permissions.
@ -217,6 +222,16 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
// If the asset does not exist, this will return a 404.
h.handler.ServeHTTP(rw, r)
return
// If requesting the install.sh script, respond with the preprocessed version
// which contains the correct hostname and version information.
case reqFile == "install.sh":
if h.installScript == nil {
http.NotFound(rw, r)
return
}
rw.Header().Add("Content-Type", "text/plain; charset=utf-8")
http.ServeContent(rw, r, reqFile, time.Time{}, bytes.NewReader(h.installScript))
return
// If the original file path exists we serve it.
case h.exists(reqFile):
if ShouldCacheFile(reqFile) {
@ -533,6 +548,32 @@ func findAndParseHTMLFiles(files fs.FS) (*template.Template, error) {
return root, nil
}
type installScriptState struct {
Origin string
Version string
}
func parseInstallScript(files fs.FS, buildInfo codersdk.BuildInfoResponse) ([]byte, error) {
scriptFile, err := fs.ReadFile(files, "install.sh")
if err != nil {
return nil, err
}
script, err := template.New("install.sh").Parse(string(scriptFile))
if err != nil {
return nil, err
}
var buf bytes.Buffer
state := installScriptState{Origin: buildInfo.DashboardURL, Version: buildInfo.Version}
err = script.Execute(&buf, state)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// ExtractOrReadBinFS checks the provided fs for compressed coder binaries and
// extracts them into dest/bin if found. As a fallback, the provided FS is
// checked for a /bin directory, if it is non-empty it is returned. Finally

View File

@ -207,6 +207,9 @@ func TestServingFiles(t *testing.T) {
"dashboard.css": &fstest.MapFile{
Data: []byte("dashboard-css-bytes"),
},
"install.sh": &fstest.MapFile{
Data: []byte("install-sh-bytes"),
},
}
binFS := http.FS(fstest.MapFS{})
@ -248,6 +251,9 @@ func TestServingFiles(t *testing.T) {
// JS, CSS cases
{"/dashboard.js", "dashboard-js-bytes"},
{"/dashboard.css", "dashboard-css-bytes"},
// Install script
{"/install.sh", "install-sh-bytes"},
}
for _, testCase := range testCases {

431
site/static/install.sh Normal file
View File

@ -0,0 +1,431 @@
#!/bin/sh
set -eu
# Coder's automatic install script.
# See https://github.com/coder/coder#install
#
# To run:
# curl -fsSL "{{ .Origin }}/install.sh" | sh
usage() {
arg0="$0"
if [ "$0" = sh ]; then
arg0="curl -fsSL \"{{ .Origin }}/install.sh\" | sh -s --"
else
not_curl_usage="The latest script is available at {{ .Origin }}/install.sh
"
fi
cath <<EOF
Installs the Coder CLI.
A matching version of the CLI will be downloaded from this Coder deployment.
Pass in user@host to install the CLI on user@host over ssh.
The remote host must have internet access.
${not_curl_usage-}
Usage:
${arg0} [--dry-run] [--prefix ~/.local] [--rsh ssh] [user@host]
--dry-run
Echo the commands for the install process without running them.
--prefix <dir>
Sets the prefix used by standalone release archives. Defaults to /usr/local
and the binary is copied into /usr/local/bin
To install in \$HOME, pass --prefix=\$HOME/.local
--binary-name <name>
Sets the name for the CLI in standalone release archives. Defaults to "coder"
To use the CLI as coder2, pass --binary-name=coder2
Note: in-product documentation will always refer to the CLI as "coder"
--rsh <bin>
Specifies the remote shell for remote installation. Defaults to ssh.
We build releases on GitHub for amd64 and arm64 on Windows, Linux, and macOS, as
well as armv7 on Linux.
The installer will cache all downloaded assets into ~/.cache/coder
EOF
}
echo_standalone_postinstall() {
if [ "${DRY_RUN-}" ]; then
echo_dryrun_postinstall
return
fi
cath <<EOF
Coder {{ .Version }} installed.
The Coder binary has been placed in the following location:
$STANDALONE_INSTALL_PREFIX/bin/$STANDALONE_BINARY_NAME
EOF
CODER_COMMAND="$(command -v "$STANDALONE_BINARY_NAME" || true)"
if [ -z "${CODER_COMMAND}" ]; then
cath <<EOF
Extend your path to use Coder:
$ PATH="$STANDALONE_INSTALL_PREFIX/bin:\$PATH"
EOF
elif [ "$CODER_COMMAND" != "$STANDALONE_BINARY_LOCATION" ]; then
echo_path_conflict "$CODER_COMMAND"
else
cath <<EOF
To run a Coder server:
$ $STANDALONE_BINARY_NAME server
To connect to a Coder deployment:
$ $STANDALONE_BINARY_NAME login <deployment url>
EOF
fi
}
echo_dryrun_postinstall() {
cath <<EOF
Dry-run complete.
To install Coder, re-run this script without the --dry-run flag.
EOF
}
echo_path_conflict() {
cath <<EOF
There is another binary in your PATH that conflicts with the binary we've installed.
$1
This is likely because of an existing installation of Coder in your \$PATH.
Run \`which -a coder\` to view all installations.
EOF
}
main() {
if [ "${TRACE-}" ]; then
set -x
fi
unset \
DRY_RUN \
ORIGIN \
ALL_FLAGS \
RSH_ARGS \
RSH
ORIGIN="{{ .Origin }}"
ALL_FLAGS=""
while [ "$#" -gt 0 ]; do
case "$1" in
-*)
ALL_FLAGS="${ALL_FLAGS} $1"
;;
esac
case "$1" in
--dry-run)
DRY_RUN=1
;;
--origin)
ORIGIN="$(parse_arg "$@")"
shift
;;
--origin=*)
ORIGIN="$(parse_arg "$@")"
;;
--prefix)
STANDALONE_INSTALL_PREFIX="$(parse_arg "$@")"
shift
;;
--prefix=*)
STANDALONE_INSTALL_PREFIX="$(parse_arg "$@")"
;;
--binary-name)
STANDALONE_BINARY_NAME="$(parse_arg "$@")"
shift
;;
--binary-name=*)
STANDALONE_BINARY_NAME="$(parse_arg "$@")"
;;
--rsh)
RSH="$(parse_arg "$@")"
shift
;;
--rsh=*)
RSH="$(parse_arg "$@")"
;;
-h | --h | -help | --help)
usage
exit 0
;;
--)
shift
# We remove the -- added above.
ALL_FLAGS="${ALL_FLAGS% --}"
RSH_ARGS="$*"
break
;;
-*)
echoerr "Unknown flag $1"
echoerr "Run with --help to see usage."
exit 1
;;
*)
RSH_ARGS="$*"
break
;;
esac
shift
done
if [ "${RSH_ARGS-}" ]; then
RSH="${RSH-ssh}"
echoh "Installing remotely with $RSH $RSH_ARGS"
curl -fsSL "$ORIGIN/install.sh" | prefix "$RSH_ARGS" "$RSH" "$RSH_ARGS" sh -s -- "$ALL_FLAGS"
return
fi
# These can be overridden for testing but shouldn't normally be used as it can
# result in a broken coder.
OS=${OS:-$(os)}
ARCH=${ARCH:-$(arch)}
# These are used by the various install_* functions that make use of GitHub
# releases in order to download and unpack the right release.
CACHE_DIR=$(echo_cache_dir)
STANDALONE_INSTALL_PREFIX=${STANDALONE_INSTALL_PREFIX:-/usr/local}
STANDALONE_BINARY_NAME=${STANDALONE_BINARY_NAME:-coder}
if [ "${DRY_RUN-}" ]; then
echoh "Running with --dry-run; the following are the commands that would be run if this were a real installation:"
echoh
fi
if ! has_standalone; then
echoerr "There is no binary for $OS-$ARCH"
exit 1
fi
install_standalone
}
parse_arg() {
case "$1" in
*=*)
# Remove everything after first equal sign.
opt="${1%%=*}"
# Remove everything before first equal sign.
optarg="${1#*=}"
if [ ! "$optarg" ]; then
echoerr "$opt requires an argument"
echoerr "Run with --help to see usage."
exit 1
fi
echo "$optarg"
return
;;
esac
case "${2-}" in
"" | -*)
echoerr "$1 requires an argument"
echoerr "Run with --help to see usage."
exit 1
;;
*)
echo "$2"
return
;;
esac
}
fetch() {
URL="$1"
FILE="$2"
if [ -e "$FILE" ]; then
echoh "+ Reusing $FILE"
return
fi
sh_c mkdir -p "$CACHE_DIR"
sh_c curl \
-#fL \
-o "$FILE.incomplete" \
-C - \
"$URL"
sh_c mv "$FILE.incomplete" "$FILE"
}
install_standalone() {
echoh "Installing coder-$OS-$ARCH {{ .Version }} from $ORIGIN."
echoh
BINARY_FILE="$CACHE_DIR/coder-${OS}-${ARCH}-{{ .Version }}"
fetch "$ORIGIN/bin/coder-${OS}-${ARCH}" "$BINARY_FILE"
# -w only works if the directory exists so try creating it first. If this
# fails we can ignore the error as the -w check will then swap us to sudo.
sh_c mkdir -p "$STANDALONE_INSTALL_PREFIX" 2>/dev/null || true
STANDALONE_BINARY_LOCATION="$STANDALONE_INSTALL_PREFIX/bin/$STANDALONE_BINARY_NAME"
sh_c="sh_c"
if [ ! -w "$STANDALONE_INSTALL_PREFIX" ]; then
sh_c="sudo_sh_c"
fi
"$sh_c" mkdir -p "$STANDALONE_INSTALL_PREFIX/bin"
# Remove the file if it already exists to
# avoid https://github.com/coder/coder/issues/2086
if [ -f "$STANDALONE_BINARY_LOCATION" ]; then
"$sh_c" rm "$STANDALONE_BINARY_LOCATION"
fi
# Copy the binary to the correct location.
"$sh_c" cp "$BINARY_FILE" "$STANDALONE_BINARY_LOCATION"
"$sh_c" chmod +x "$STANDALONE_BINARY_LOCATION"
echo_standalone_postinstall
}
# Determine if we have standalone releases on GitHub for the system's arch.
has_standalone() {
case $ARCH in
amd64) return 0 ;;
arm64) return 0 ;;
armv7)
[ "$(distro)" = "linux" ]
return
;;
*) return 1 ;;
esac
}
os() {
uname="$(uname)"
case $uname in
Linux) echo linux ;;
Darwin) echo darwin ;;
FreeBSD) echo freebsd ;;
*) echo "$uname" ;;
esac
}
# Print a human-readable name for the OS/distro.
distro_name() {
if [ "$(uname)" = "Darwin" ]; then
echo "macOS v$(sw_vers -productVersion)"
return
fi
if [ -f /etc/os-release ]; then
(
# shellcheck disable=SC1091
. /etc/os-release
echo "$PRETTY_NAME"
)
return
fi
# Prints something like: Linux 4.19.0-9-amd64
uname -sr
}
arch() {
uname_m=$(uname -m)
case $uname_m in
aarch64) echo arm64 ;;
x86_64) echo amd64 ;;
armv7l) echo armv7 ;;
*) echo "$uname_m" ;;
esac
}
command_exists() {
if [ ! "$1" ]; then return 1; fi
command -v "$@" >/dev/null
}
sh_c() {
echoh "+ $*"
if [ ! "${DRY_RUN-}" ]; then
sh -c "$*"
fi
}
sudo_sh_c() {
if [ "$(id -u)" = 0 ]; then
sh_c "$@"
elif command_exists sudo; then
sh_c "sudo $*"
elif command_exists doas; then
sh_c "doas $*"
elif command_exists su; then
sh_c "su - -c '$*'"
else
echoh
echoerr "This script needs to run the following command as root."
echoerr " $*"
echoerr "Please install sudo, su, or doas."
exit 1
fi
}
echo_cache_dir() {
if [ "${XDG_CACHE_HOME-}" ]; then
echo "$XDG_CACHE_HOME/coder/local_downloads"
elif [ "${HOME-}" ]; then
echo "$HOME/.cache/coder/local_downloads"
else
echo "/tmp/coder-cache/local_downloads"
fi
}
echoh() {
echo "$@" | humanpath
}
cath() {
humanpath
}
echoerr() {
echoh "$@" >&2
}
# humanpath replaces all occurrences of " $HOME" with " ~"
# and all occurrences of '"$HOME' with the literal '"$HOME'.
humanpath() {
sed "s# $HOME# ~#g; s#\"$HOME#\"\$HOME#g"
}
# We need to make sure we exit with a non zero exit if the command fails.
# /bin/sh does not support -o pipefail unfortunately.
prefix() {
PREFIX="$1"
shift
fifo="$(mktemp -d)/fifo"
mkfifo "$fifo"
sed -e "s#^#$PREFIX: #" "$fifo" &
"$@" >"$fifo" 2>&1
}
main "$@"