From bb5f5f2a7f2cf0269638f12c68ed09006022d5af Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 13 Jan 2025 18:45:01 +0000 Subject: [PATCH 1/3] feat: bundle a local version of install.sh --- Makefile | 19 +- site/site.go | 43 ++++- site/site_test.go | 6 + site/static/install.sh | 425 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 487 insertions(+), 6 deletions(-) create mode 100644 site/static/install.sh diff --git a/Makefile b/Makefile index 423402260c26b..71bcef76aee70 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/site/site.go b/site/site.go index 1924a17fe1a10..4cdc79aa6cf11 100644 --- a/site/site.go +++ b/site/site.go @@ -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 { + _ = xerrors.Errorf("install.sh will be unavailable: %w", err) + } + 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 diff --git a/site/site_test.go b/site/site_test.go index 2acf303b2c13b..8bee665a56ae3 100644 --- a/site/site_test.go +++ b/site/site_test.go @@ -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 { diff --git a/site/static/install.sh b/site/static/install.sh new file mode 100644 index 0000000000000..6c24761dcbc89 --- /dev/null +++ b/site/static/install.sh @@ -0,0 +1,425 @@ +#!/bin/sh +set -eu + +# Coder's automatic install script. +# See https://github.com/coder/coder#install +# +# To run: +# curl -L {{ .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 < + 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 + 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 + 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 + fi +} + +echo_dryrun_postinstall() { + cath </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 "$@" From a25502f6c4eb1125d66b290ed454f1f90d440e38 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 13 Jan 2025 22:02:02 +0000 Subject: [PATCH 2/3] allow setting a custom origin, just in case --- site/static/install.sh | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/site/static/install.sh b/site/static/install.sh index 6c24761dcbc89..d81740c04b9b9 100644 --- a/site/static/install.sh +++ b/site/static/install.sh @@ -121,11 +121,12 @@ main() { unset \ DRY_RUN \ - OPTIONAL \ + ORIGIN \ ALL_FLAGS \ RSH_ARGS \ RSH + ORIGIN="{{ .Origin }}" ALL_FLAGS="" while [ "$#" -gt 0 ]; do @@ -139,6 +140,13 @@ main() { --dry-run) DRY_RUN=1 ;; + --origin) + ORIGIN="$(parse_arg "$@")" + shift + ;; + --origin=*) + ORIGIN="$(parse_arg "$@")" + ;; --prefix) STANDALONE_INSTALL_PREFIX="$(parse_arg "$@")" shift @@ -188,7 +196,7 @@ main() { 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" + curl -fsSL "$ORIGIN/install.sh" | prefix "$RSH_ARGS" "$RSH" "$RSH_ARGS" sh -s -- "$ALL_FLAGS" return fi @@ -223,7 +231,7 @@ parse_arg() { opt="${1%%=*}" # Remove everything before first equal sign. optarg="${1#*=}" - if [ ! "$optarg" ] && [ ! "${OPTIONAL-}" ]; then + if [ ! "$optarg" ]; then echoerr "$opt requires an argument" echoerr "Run with --help to see usage." exit 1 @@ -235,11 +243,9 @@ parse_arg() { case "${2-}" in "" | -*) - if [ ! "${OPTIONAL-}" ]; then - echoerr "$1 requires an argument" - echoerr "Run with --help to see usage." - exit 1 - fi + echoerr "$1 requires an argument" + echoerr "Run with --help to see usage." + exit 1 ;; *) echo "$2" @@ -267,12 +273,12 @@ fetch() { } install_standalone() { - echoh "Installing coder-$OS-$ARCH {{ .Version }} from {{ .Origin }}." + 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" + 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. From c6d37cabd2db1d2d5a6213ee58da050794ceea74 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 14 Jan 2025 17:54:51 +0000 Subject: [PATCH 3/3] feedback --- site/site.go | 2 +- site/static/install.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/site/site.go b/site/site.go index 4cdc79aa6cf11..af66c01c6f896 100644 --- a/site/site.go +++ b/site/site.go @@ -162,7 +162,7 @@ func New(opts *Options) *Handler { handler.installScript, err = parseInstallScript(opts.SiteFS, opts.BuildInfo) if err != nil { - _ = xerrors.Errorf("install.sh will be unavailable: %w", err) + _, _ = fmt.Fprintf(os.Stderr, "install.sh will be unavailable: %v", err.Error()) } return handler diff --git a/site/static/install.sh b/site/static/install.sh index d81740c04b9b9..ff01870e49bc3 100644 --- a/site/static/install.sh +++ b/site/static/install.sh @@ -5,12 +5,12 @@ set -eu # See https://github.com/coder/coder#install # # To run: -# curl -L {{ .Origin }}/install.sh | sh +# curl -fsSL "{{ .Origin }}/install.sh" | sh usage() { arg0="$0" if [ "$0" = sh ]; then - arg0="curl -fsSL {{ .Origin }}/install.sh | sh -s --" + arg0="curl -fsSL \"{{ .Origin }}/install.sh\" | sh -s --" else not_curl_usage="The latest script is available at {{ .Origin }}/install.sh "