From 430f807e1ef91714770883bfd0804fd8ea814a7b Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 8 Jan 2025 01:44:40 +0000 Subject: [PATCH 1/8] feat: bundle a local version of install.sh, add CLI install page --- .../components/CodeExample/CodeExample.tsx | 2 +- .../UserDropdown/UserDropdownContent.tsx | 9 +- .../pages/CliInstallPage/CliInstallPage.tsx | 17 + .../CliInstallPageView.stories.tsx | 17 + .../CliInstallPage/CliInstallPageView.tsx | 60 ++ site/src/router.tsx | 10 +- site/static/install.sh | 846 ++++++++++++++++++ 7 files changed, 956 insertions(+), 5 deletions(-) create mode 100644 site/src/pages/CliInstallPage/CliInstallPage.tsx create mode 100644 site/src/pages/CliInstallPage/CliInstallPageView.stories.tsx create mode 100644 site/src/pages/CliInstallPage/CliInstallPageView.tsx create mode 100644 site/static/install.sh diff --git a/site/src/components/CodeExample/CodeExample.tsx b/site/src/components/CodeExample/CodeExample.tsx index 35a7094ae3cc0..71ef7f951471e 100644 --- a/site/src/components/CodeExample/CodeExample.tsx +++ b/site/src/components/CodeExample/CodeExample.tsx @@ -65,7 +65,7 @@ export const CodeExample: FC = ({ ) : ( - <>{code} + code )} diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx index a6a3d2f3d0a1b..90ea1dab74a67 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx @@ -8,6 +8,7 @@ import AccountIcon from "@mui/icons-material/AccountCircleOutlined"; import BugIcon from "@mui/icons-material/BugReportOutlined"; import ChatIcon from "@mui/icons-material/ChatOutlined"; import LogoutIcon from "@mui/icons-material/ExitToAppOutlined"; +import InstallDesktopIcon from "@mui/icons-material/InstallDesktop"; import LaunchIcon from "@mui/icons-material/LaunchOutlined"; import DocsIcon from "@mui/icons-material/MenuBook"; import Divider from "@mui/material/Divider"; @@ -21,7 +22,6 @@ import { Stack } from "components/Stack/Stack"; import { usePopover } from "components/deprecated/Popover/Popover"; import type { FC } from "react"; import { Link } from "react-router-dom"; - export const Language = { accountLabel: "Account", signOutLabel: "Sign Out", @@ -76,6 +76,13 @@ export const UserDropdownContent: FC = ({ + + + + Install CLI + + + diff --git a/site/src/pages/CliInstallPage/CliInstallPage.tsx b/site/src/pages/CliInstallPage/CliInstallPage.tsx new file mode 100644 index 0000000000000..e9a7b4db3f343 --- /dev/null +++ b/site/src/pages/CliInstallPage/CliInstallPage.tsx @@ -0,0 +1,17 @@ +import type { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { CliInstallPageView } from "./CliInstallPageView"; + +export const CliInstallPage: FC = () => { + return ( + <> + + {pageTitle("Install the Coder CLI")} + + + + ); +}; + +export default CliInstallPage; diff --git a/site/src/pages/CliInstallPage/CliInstallPageView.stories.tsx b/site/src/pages/CliInstallPage/CliInstallPageView.stories.tsx new file mode 100644 index 0000000000000..425d3306dce89 --- /dev/null +++ b/site/src/pages/CliInstallPage/CliInstallPageView.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { CliInstallPageView } from "./CliInstallPageView"; + +const meta: Meta = { + title: "pages/CliAuthPage", + component: CliInstallPageView, + args: { + sessionToken: "some-session-token", + }, +}; + +export default meta; +type Story = StoryObj; + +const Example: Story = {}; + +export { Example as CliAuthPage }; diff --git a/site/src/pages/CliInstallPage/CliInstallPageView.tsx b/site/src/pages/CliInstallPage/CliInstallPageView.tsx new file mode 100644 index 0000000000000..1c349c8bb8d21 --- /dev/null +++ b/site/src/pages/CliInstallPage/CliInstallPageView.tsx @@ -0,0 +1,60 @@ +import type { Interpolation, Theme } from "@emotion/react"; +import { CodeExample } from "components/CodeExample/CodeExample"; +import { SignInLayout } from "components/SignInLayout/SignInLayout"; +import { Welcome } from "components/Welcome/Welcome"; +import type { FC } from "react"; +import { Link as RouterLink } from "react-router-dom"; + +export const CliInstallPageView: FC = () => { + const origin = location.origin; + + return ( + + Install the Coder CLI + +

+ Copy the command below and{" "} + paste it in your terminal. +

+ + + +
+ + Go to workspaces + +
+
+ ); +}; + +const styles = { + instructions: (theme) => ({ + fontSize: 16, + color: theme.palette.text.secondary, + paddingBottom: 8, + textAlign: "center", + lineHeight: 1.4, + + // Have to undo styling side effects from component + marginTop: -24, + }), + + backLink: (theme) => ({ + display: "block", + textAlign: "center", + color: theme.palette.text.primary, + textDecoration: "underline", + textUnderlineOffset: 3, + textDecorationColor: "hsla(0deg, 0%, 100%, 0.7)", + paddingTop: 16, + paddingBottom: 16, + + "&:hover": { + textDecoration: "none", + }, + }), +} satisfies Record>; diff --git a/site/src/router.tsx b/site/src/router.tsx index 6e6fe630f7188..c292b8b8a72f8 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -37,8 +37,9 @@ const DeploymentSettingsProvider = lazy( const OrganizationSettingsLayout = lazy( () => import("./modules/management/OrganizationSettingsLayout"), ); -const CliAuthenticationPage = lazy( - () => import("./pages/CliAuthPage/CliAuthPage"), +const CliAuthPage = lazy(() => import("./pages/CliAuthPage/CliAuthPage")); +const CliInstallPage = lazy( + () => import("./pages/CliInstallPage/CliInstallPage"), ); const AccountPage = lazy( () => import("./pages/UserSettingsPage/AccountPage/AccountPage"), @@ -539,6 +540,9 @@ export const router = createBrowserRouter( element={} /> + + } /> + {/* Using path="*"" means "match anything", so this route acts like a catch-all for URLs that we don't have explicit routes for. */} @@ -559,7 +563,7 @@ export const router = createBrowserRouter( path="/:username/:workspace/terminal" element={} /> - } /> + } /> } /> , diff --git a/site/static/install.sh b/site/static/install.sh new file mode 100644 index 0000000000000..e6a553eaac1fb --- /dev/null +++ b/site/static/install.sh @@ -0,0 +1,846 @@ +#!/bin/sh +set -eu + +# Coder's automatic install script. +# See https://github.com/coder/coder#install +# +# To run: +# curl -L https://coder.com/install.sh | sh + +usage() { + arg0="$0" + if [ "$0" = sh ]; then + arg0="curl -fsSL https://coder.com/install.sh | sh -s --" + else + not_curl_usage="The latest script is available at https://coder.com/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. + + --with-terraform + Installs Terraform binary from https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/ source + alongside coder. + This is great for if you are having issues with Coder installing terraform, or if you + just want it on your base system aswell. + This supports most systems, however if you are unsure yours is supported you can check + the link above. + --net-admin + Adds \`CAP_NET_ADMIN\` to the installed binary. This allows Coder to + increase network speeds, but has security implications. + See: https://man7.org/linux/man-pages/man7/capabilities.7.html + This only works on Linux based systems. + + +The detection method works as follows: + - Debian, Ubuntu, Raspbian: install the deb package from GitHub. + - Fedora, CentOS, RHEL, openSUSE: install the rpm package from GitHub. + - Alpine: install the apk package from GitHub. + - macOS: if \`brew\` is available, install from the coder/coder Homebrew tap. + - Otherwise, download from GitHub and install into \`--prefix\`. + +We build releases on GitHub for amd64, armv7, and arm64 on Windows, Linux, and macOS. + +When the detection method tries to pull a release from GitHub it will +fall back to installing standalone when there is no matching release for +the system's operating system and architecture. + +The installer will cache all downloaded assets into ~/.cache/coder +EOF +} + +echo_latest_stable_version() { + # https://gist.github.com/lukechilds/a83e1d7127b78fef38c2914c4ececc3c#gistcomment-2758860 + version="$(curl -fsSLI -o /dev/null -w "%{url_effective}" https://github.com/coder/coder/releases/latest)" + version="${version#https://github.com/coder/coder/releases/tag/v}" + echo "${version}" +} + +echo_latest_mainline_version() { + # Fetch the releases from the GitHub API, sort by version number, + # and take the first result. Note that we're sorting by space- + # separated numbers and without utilizing the sort -V flag for the + # best compatibility. + curl -fsSL https://api.github.com/repos/coder/coder/releases | + awk -F'"' '/"tag_name"/ {print $4}' | + tr -d v | + tr . ' ' | + sort -k1,1nr -k2,2nr -k3,3nr | + head -n1 | + tr ' ' . +} + +echo_standalone_postinstall() { + if [ "${DRY_RUN-}" ]; then + echo_dryrun_postinstall + return + fi + + channel= + advisory="To install our stable release (v${STABLE_VERSION}), use the --stable flag. " + if [ "${STABLE}" = 1 ]; then + channel="stable " + advisory="" + fi + if [ "${MAINLINE}" = 1 ]; then + channel="mainline " + fi + + cath < + +EOF + fi +} + +echo_brew_postinstall() { + if [ "${DRY_RUN-}" ]; then + echo_dryrun_postinstall + return + fi + + BREW_PREFIX="$(brew --prefix)" + + cath < + +EOF +} + +echo_systemd_postinstall() { + if [ "${DRY_RUN-}" ]; then + echo_dryrun_postinstall + return + fi + + echoh + cath < + +EOF +} + +echo_dryrun_postinstall() { + cath </dev/null || true + + sh_c="sh_c" + if [ ! -w "$TERRAFORM_INSTALL_PREFIX" ]; then + sh_c="sudo_sh_c" + fi + # Prepare /usr/local/bin/ and the binary for copying + "$sh_c" mkdir -p "$TERRAFORM_INSTALL_PREFIX/bin" + "$sh_c" unzip -d "$CACHE_DIR" -o "$CACHE_DIR/terraform_${TERRAFORM_VERSION}_${OS}_${ARCH}.zip" + COPY_LOCATION="$TERRAFORM_INSTALL_PREFIX/bin/terraform" + + # Remove the file if it already exists to + # avoid https://github.com/coder/coder/issues/2086 + if [ -f "$COPY_LOCATION" ]; then + "$sh_c" rm "$COPY_LOCATION" + fi + + # Copy the binary to the correct location. + "$sh_c" cp "$CACHE_DIR/terraform" "$COPY_LOCATION" +} + +install_macos() { + # If there is no `brew` binary available, just default to installing standalone + if command_exists brew; then + echoh "Installing coder with Homebrew from the coder/coder tap." + echoh + + sh_c brew install coder/coder/coder + + echo_brew_postinstall + return + fi + + echoh "Homebrew is not available." + echoh "Falling back to standalone installation." + install_standalone +} + +install_deb() { + echoh "Installing v$VERSION of the $ARCH deb package from GitHub." + echoh + + fetch "https://github.com/coder/coder/releases/download/v$VERSION/coder_${VERSION}_${OS}_${ARCH}.deb" \ + "$CACHE_DIR/coder_${VERSION}_$ARCH.deb" + sudo_sh_c dpkg --force-confdef --force-confold -i "$CACHE_DIR/coder_${VERSION}_$ARCH.deb" + + echo_systemd_postinstall deb +} + +install_rpm() { + echoh "Installing v$VERSION of the $ARCH rpm package from GitHub." + echoh + + fetch "https://github.com/coder/coder/releases/download/v$VERSION/coder_${VERSION}_${OS}_${ARCH}.rpm" \ + "$CACHE_DIR/coder_${VERSION}_${OS}_${ARCH}.rpm" + sudo_sh_c rpm -U "$CACHE_DIR/coder_${VERSION}_${OS}_${ARCH}.rpm" + + echo_systemd_postinstall rpm +} + +install_apk() { + echoh "Installing v$VERSION of the $ARCH apk package from GitHub." + echoh + + fetch "https://github.com/coder/coder/releases/download/v$VERSION/coder_${VERSION}_${OS}_${ARCH}.apk" \ + "$CACHE_DIR/coder_${VERSION}_${OS}_${ARCH}.apk" + sudo_sh_c apk add --allow-untrusted "$CACHE_DIR/coder_${VERSION}_${OS}_${ARCH}.apk" + + echo_systemd_postinstall apk +} + +install_standalone() { + echoh "Installing v$VERSION of the $ARCH release from GitHub." + echoh + + # macOS releases are packaged as .zip + case $OS in + darwin) STANDALONE_ARCHIVE_FORMAT=zip ;; + *) STANDALONE_ARCHIVE_FORMAT=tar.gz ;; + esac + + fetch "https://github.com/coder/coder/releases/download/v$VERSION/coder_${VERSION}_${OS}_${ARCH}.$STANDALONE_ARCHIVE_FORMAT" \ + "$CACHE_DIR/coder_${VERSION}_${OS}_${ARCH}.$STANDALONE_ARCHIVE_FORMAT" + + # -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 + + sh_c mkdir -p "$CACHE_DIR/tmp" + if [ "$STANDALONE_ARCHIVE_FORMAT" = tar.gz ]; then + sh_c tar -C "$CACHE_DIR/tmp" -xzf "$CACHE_DIR/coder_${VERSION}_${OS}_${ARCH}.tar.gz" + else + sh_c unzip -d "$CACHE_DIR/tmp" -o "$CACHE_DIR/coder_${VERSION}_${OS}_${ARCH}.zip" + fi + + 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 "$CACHE_DIR/tmp/coder" "$STANDALONE_BINARY_LOCATION" + + # Clean up the extracted files (note, not using sudo: $sh_c -> sh_c). + sh_c rm -rv "$CACHE_DIR/tmp" + + 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)" != darwin ] + return + ;; + *) return 1 ;; + esac +} + +os() { + uname="$(uname)" + case $uname in + Linux) echo linux ;; + Darwin) echo darwin ;; + FreeBSD) echo freebsd ;; + *) echo "$uname" ;; + esac +} + +# Print the detected Linux distro, otherwise print the OS name. +# +# Example outputs: +# - darwin -> darwin +# - freebsd -> freebsd +# - ubuntu, raspbian, debian ... -> debian +# - amzn, centos, rhel, fedora, ... -> fedora +# - opensuse-{leap,tumbleweed} -> opensuse +# - alpine -> alpine +# - arch -> arch +# +# Inspired by https://github.com/docker/docker-install/blob/26ff363bcf3b3f5a00498ac43694bf1c7d9ce16c/install.sh#L111-L120. +distro() { + if [ "$OS" = "darwin" ] || [ "$OS" = "freebsd" ]; then + echo "$OS" + return + fi + + if [ -f /etc/os-release ]; then + ( + # shellcheck disable=SC1091 + . /etc/os-release + if [ "${ID_LIKE-}" ]; then + for id_like in $ID_LIKE; do + case "$id_like" in debian | fedora | opensuse) + echo "$id_like" + return + ;; + esac + done + fi + + echo "$ID" + ) + return + fi +} + +# 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 +} + +# The following is to change the naming, that way people with armv7 won't receive a error +# List of binaries can be found here: https://releases.hashicorp.com/terraform/ +terraform_arch() { + uname_m=$(uname -m) + case $uname_m in + aarch64) echo arm64 ;; + x86_64) echo amd64 ;; + armv7l) echo arm ;; + *) 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" + elif [ "${HOME-}" ]; then + echo "$HOME/.cache/coder" + else + echo "/tmp/coder-cache" + 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 6a0d1cb1f41f28a48f20e35c89b6ff21100b7428 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Sat, 11 Jan 2025 00:23:31 +0000 Subject: [PATCH 2/8] yaaay --- Makefile | 18 +- site/site.go | 43 +++- site/static/install.sh | 479 +++-------------------------------------- 3 files changed, 84 insertions(+), 456 deletions(-) diff --git a/Makefile b/Makefile index 2cd40a7dabfa3..c6ad76b21ad5d 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/ @@ -542,22 +552,20 @@ GEN_FILES := \ vpn/vpn.pb.go \ coderd/database/dump.sql \ $(DB_GEN_FILES) \ - site/src/api/typesGenerated.ts \ + $(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_FILES) .PHONY: gen diff --git a/site/site.go b/site/site.go index 1924a17fe1a10..0205ea9806f00 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 { + panic("failed to parse install script") + } + 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,11 @@ 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, fill in the template with the + // appropriate hostname and version info. + case reqFile == "install.sh": + h.serveInstallScript(rw, r) + return // If the original file path exists we serve it. case h.exists(reqFile): if ShouldCacheFile(reqFile) { @@ -306,6 +316,11 @@ func ShouldCacheFile(reqFile string) bool { return true } +func (h *Handler) serveInstallScript(rw http.ResponseWriter, r *http.Request) { + rw.Header().Add("content-type", "text/plain; charset=utf-8") + http.ServeContent(rw, r, "install.sh", time.Time{}, bytes.NewReader(h.installScript)) +} + func (h *Handler) serveHTML(resp http.ResponseWriter, request *http.Request, reqPath string, state htmlState) bool { if data, err := h.renderHTMLWithState(request, reqPath, state); err == nil { if reqPath == "" { @@ -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 []byte{}, err + } + + script, err := template.New("install.sh").Parse(string(scriptFile)) + if err != nil { + return []byte{}, err + } + + var buf bytes.Buffer + state := installScriptState{Origin: buildInfo.DashboardURL, Version: buildInfo.Version} + err = script.Execute(&buf, state) + if err != nil { + return []byte{}, err + } + + return buf.Bytes(), err +} + // 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/static/install.sh b/site/static/install.sh index e6a553eaac1fb..7e4e9d7691dad 100644 --- a/site/static/install.sh +++ b/site/static/install.sh @@ -5,132 +5,61 @@ set -eu # See https://github.com/coder/coder#install # # To run: -# curl -L https://coder.com/install.sh | sh +# curl -L {{ .Origin }}/install.sh | sh usage() { arg0="$0" if [ "$0" = sh ]; then - arg0="curl -fsSL https://coder.com/install.sh | sh -s --" + arg0="curl -fsSL {{ .Origin }}/install.sh | sh -s --" else - not_curl_usage="The latest script is available at https://coder.com/install.sh + 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" + 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. - --with-terraform - Installs Terraform binary from https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/ source - alongside coder. - This is great for if you are having issues with Coder installing terraform, or if you - just want it on your base system aswell. - This supports most systems, however if you are unsure yours is supported you can check - the link above. - --net-admin - Adds \`CAP_NET_ADMIN\` to the installed binary. This allows Coder to - increase network speeds, but has security implications. - See: https://man7.org/linux/man-pages/man7/capabilities.7.html - This only works on Linux based systems. - - -The detection method works as follows: - - Debian, Ubuntu, Raspbian: install the deb package from GitHub. - - Fedora, CentOS, RHEL, openSUSE: install the rpm package from GitHub. - - Alpine: install the apk package from GitHub. - - macOS: if \`brew\` is available, install from the coder/coder Homebrew tap. - - Otherwise, download from GitHub and install into \`--prefix\`. - -We build releases on GitHub for amd64, armv7, and arm64 on Windows, Linux, and macOS. - -When the detection method tries to pull a release from GitHub it will -fall back to installing standalone when there is no matching release for -the system's operating system and architecture. + +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_latest_stable_version() { - # https://gist.github.com/lukechilds/a83e1d7127b78fef38c2914c4ececc3c#gistcomment-2758860 - version="$(curl -fsSLI -o /dev/null -w "%{url_effective}" https://github.com/coder/coder/releases/latest)" - version="${version#https://github.com/coder/coder/releases/tag/v}" - echo "${version}" -} - -echo_latest_mainline_version() { - # Fetch the releases from the GitHub API, sort by version number, - # and take the first result. Note that we're sorting by space- - # separated numbers and without utilizing the sort -V flag for the - # best compatibility. - curl -fsSL https://api.github.com/repos/coder/coder/releases | - awk -F'"' '/"tag_name"/ {print $4}' | - tr -d v | - tr . ' ' | - sort -k1,1nr -k2,2nr -k3,3nr | - head -n1 | - tr ' ' . -} - echo_standalone_postinstall() { if [ "${DRY_RUN-}" ]; then echo_dryrun_postinstall return fi - channel= - advisory="To install our stable release (v${STABLE_VERSION}), use the --stable flag. " - if [ "${STABLE}" = 1 ]; then - channel="stable " - advisory="" - fi - if [ "${MAINLINE}" = 1 ]; then - channel="mainline " - fi - cath < - -EOF -} - -echo_systemd_postinstall() { - if [ "${DRY_RUN-}" ]; then - echo_dryrun_postinstall - return - fi - - echoh - cath < - -EOF -} - echo_dryrun_postinstall() { cath </dev/null || true - - sh_c="sh_c" - if [ ! -w "$TERRAFORM_INSTALL_PREFIX" ]; then - sh_c="sudo_sh_c" - fi - # Prepare /usr/local/bin/ and the binary for copying - "$sh_c" mkdir -p "$TERRAFORM_INSTALL_PREFIX/bin" - "$sh_c" unzip -d "$CACHE_DIR" -o "$CACHE_DIR/terraform_${TERRAFORM_VERSION}_${OS}_${ARCH}.zip" - COPY_LOCATION="$TERRAFORM_INSTALL_PREFIX/bin/terraform" - - # Remove the file if it already exists to - # avoid https://github.com/coder/coder/issues/2086 - if [ -f "$COPY_LOCATION" ]; then - "$sh_c" rm "$COPY_LOCATION" - fi - - # Copy the binary to the correct location. - "$sh_c" cp "$CACHE_DIR/terraform" "$COPY_LOCATION" -} - -install_macos() { - # If there is no `brew` binary available, just default to installing standalone - if command_exists brew; then - echoh "Installing coder with Homebrew from the coder/coder tap." - echoh - - sh_c brew install coder/coder/coder - - echo_brew_postinstall - return - fi - - echoh "Homebrew is not available." - echoh "Falling back to standalone installation." - install_standalone -} - -install_deb() { - echoh "Installing v$VERSION of the $ARCH deb package from GitHub." - echoh - - fetch "https://github.com/coder/coder/releases/download/v$VERSION/coder_${VERSION}_${OS}_${ARCH}.deb" \ - "$CACHE_DIR/coder_${VERSION}_$ARCH.deb" - sudo_sh_c dpkg --force-confdef --force-confold -i "$CACHE_DIR/coder_${VERSION}_$ARCH.deb" - - echo_systemd_postinstall deb -} - -install_rpm() { - echoh "Installing v$VERSION of the $ARCH rpm package from GitHub." - echoh - - fetch "https://github.com/coder/coder/releases/download/v$VERSION/coder_${VERSION}_${OS}_${ARCH}.rpm" \ - "$CACHE_DIR/coder_${VERSION}_${OS}_${ARCH}.rpm" - sudo_sh_c rpm -U "$CACHE_DIR/coder_${VERSION}_${OS}_${ARCH}.rpm" - - echo_systemd_postinstall rpm -} - -install_apk() { - echoh "Installing v$VERSION of the $ARCH apk package from GitHub." - echoh - - fetch "https://github.com/coder/coder/releases/download/v$VERSION/coder_${VERSION}_${OS}_${ARCH}.apk" \ - "$CACHE_DIR/coder_${VERSION}_${OS}_${ARCH}.apk" - sudo_sh_c apk add --allow-untrusted "$CACHE_DIR/coder_${VERSION}_${OS}_${ARCH}.apk" - - echo_systemd_postinstall apk -} - install_standalone() { - echoh "Installing v$VERSION of the $ARCH release from GitHub." + echoh "Installing coder-$OS-$ARCH {{ .Version }} from {{ .Origin }}." echoh - # macOS releases are packaged as .zip - case $OS in - darwin) STANDALONE_ARCHIVE_FORMAT=zip ;; - *) STANDALONE_ARCHIVE_FORMAT=tar.gz ;; - esac + BINARY_FILE="$CACHE_DIR/coder-${OS}-${ARCH}-{{ .Version }}" - fetch "https://github.com/coder/coder/releases/download/v$VERSION/coder_${VERSION}_${OS}_${ARCH}.$STANDALONE_ARCHIVE_FORMAT" \ - "$CACHE_DIR/coder_${VERSION}_${OS}_${ARCH}.$STANDALONE_ARCHIVE_FORMAT" + 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 - sh_c mkdir -p "$CACHE_DIR/tmp" - if [ "$STANDALONE_ARCHIVE_FORMAT" = tar.gz ]; then - sh_c tar -C "$CACHE_DIR/tmp" -xzf "$CACHE_DIR/coder_${VERSION}_${OS}_${ARCH}.tar.gz" - else - sh_c unzip -d "$CACHE_DIR/tmp" -o "$CACHE_DIR/coder_${VERSION}_${OS}_${ARCH}.zip" - fi - STANDALONE_BINARY_LOCATION="$STANDALONE_INSTALL_PREFIX/bin/$STANDALONE_BINARY_NAME" sh_c="sh_c" @@ -663,10 +294,8 @@ install_standalone() { fi # Copy the binary to the correct location. - "$sh_c" cp "$CACHE_DIR/tmp/coder" "$STANDALONE_BINARY_LOCATION" - - # Clean up the extracted files (note, not using sudo: $sh_c -> sh_c). - sh_c rm -rv "$CACHE_DIR/tmp" + "$sh_c" cp "$BINARY_FILE" "$STANDALONE_BINARY_LOCATION" + "$sh_c" chmod +x "$STANDALONE_BINARY_LOCATION" echo_standalone_postinstall } @@ -677,7 +306,7 @@ has_standalone() { amd64) return 0 ;; arm64) return 0 ;; armv7) - [ "$(distro)" != darwin ] + [ "$(distro)" == "linux" ] return ;; *) return 1 ;; @@ -694,44 +323,6 @@ os() { esac } -# Print the detected Linux distro, otherwise print the OS name. -# -# Example outputs: -# - darwin -> darwin -# - freebsd -> freebsd -# - ubuntu, raspbian, debian ... -> debian -# - amzn, centos, rhel, fedora, ... -> fedora -# - opensuse-{leap,tumbleweed} -> opensuse -# - alpine -> alpine -# - arch -> arch -# -# Inspired by https://github.com/docker/docker-install/blob/26ff363bcf3b3f5a00498ac43694bf1c7d9ce16c/install.sh#L111-L120. -distro() { - if [ "$OS" = "darwin" ] || [ "$OS" = "freebsd" ]; then - echo "$OS" - return - fi - - if [ -f /etc/os-release ]; then - ( - # shellcheck disable=SC1091 - . /etc/os-release - if [ "${ID_LIKE-}" ]; then - for id_like in $ID_LIKE; do - case "$id_like" in debian | fedora | opensuse) - echo "$id_like" - return - ;; - esac - done - fi - - echo "$ID" - ) - return - fi -} - # Print a human-readable name for the OS/distro. distro_name() { if [ "$(uname)" = "Darwin" ]; then @@ -762,18 +353,6 @@ arch() { esac } -# The following is to change the naming, that way people with armv7 won't receive a error -# List of binaries can be found here: https://releases.hashicorp.com/terraform/ -terraform_arch() { - uname_m=$(uname -m) - case $uname_m in - aarch64) echo arm64 ;; - x86_64) echo amd64 ;; - armv7l) echo arm ;; - *) echo "$uname_m" ;; - esac -} - command_exists() { if [ ! "$1" ]; then return 1; fi command -v "$@" >/dev/null @@ -806,11 +385,11 @@ sudo_sh_c() { echo_cache_dir() { if [ "${XDG_CACHE_HOME-}" ]; then - echo "$XDG_CACHE_HOME/coder" + echo "$XDG_CACHE_HOME/coder/local_downloads" elif [ "${HOME-}" ]; then - echo "$HOME/.cache/coder" + echo "$HOME/.cache/coder/local_downloads" else - echo "/tmp/coder-cache" + echo "/tmp/coder-cache/local_downloads" fi } From c64a2efecf1d80d7b04c73130e07445e98a76047 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Sat, 11 Jan 2025 00:41:04 +0000 Subject: [PATCH 3/8] fix some tests --- site/site.go | 8 ++------ site/site_test.go | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/site/site.go b/site/site.go index 0205ea9806f00..382b4f1e8b48f 100644 --- a/site/site.go +++ b/site/site.go @@ -225,7 +225,8 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { // If requesting the install.sh script, fill in the template with the // appropriate hostname and version info. case reqFile == "install.sh": - h.serveInstallScript(rw, r) + 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): @@ -316,11 +317,6 @@ func ShouldCacheFile(reqFile string) bool { return true } -func (h *Handler) serveInstallScript(rw http.ResponseWriter, r *http.Request) { - rw.Header().Add("content-type", "text/plain; charset=utf-8") - http.ServeContent(rw, r, "install.sh", time.Time{}, bytes.NewReader(h.installScript)) -} - func (h *Handler) serveHTML(resp http.ResponseWriter, request *http.Request, reqPath string, state htmlState) bool { if data, err := h.renderHTMLWithState(request, reqPath, state); err == nil { if reqPath == "" { diff --git a/site/site_test.go b/site/site_test.go index 2acf303b2c13b..dfdc714344295 100644 --- a/site/site_test.go +++ b/site/site_test.go @@ -41,6 +41,9 @@ func TestInjection(t *testing.T) { "index.html": &fstest.MapFile{ Data: []byte("{{ .User }}"), }, + "install.sh": &fstest.MapFile{ + Data: []byte{}, + }, } binFs := http.FS(fstest.MapFS{}) db := dbmem.New() @@ -99,6 +102,9 @@ func TestInjectionFailureProducesCleanHTML(t *testing.T) { "index.html": &fstest.MapFile{ Data: []byte("{{ .User }}"), }, + "install.sh": &fstest.MapFile{ + Data: []byte{}, + }, } handler := site.New(&site.Options{ BinFS: binFs, @@ -144,6 +150,9 @@ func TestCaching(t *testing.T) { "terminal.html": &fstest.MapFile{ Data: []byte("folderFile"), }, + "install.sh": &fstest.MapFile{ + Data: []byte{}, + }, } binFS := http.FS(fstest.MapFS{}) @@ -207,6 +216,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 +260,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 { @@ -348,6 +363,9 @@ func TestServingBin(t *testing.T) { "dashboard.css": &fstest.MapFile{ Data: []byte("dashboard-css-bytes"), }, + "install.sh": &fstest.MapFile{ + Data: []byte{}, + }, } sampleBinFSCorrupted := sampleBinFS() From edd2cf3067d42a24209b681fc2d95c9b592ba464 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Sat, 11 Jan 2025 00:58:25 +0000 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/static/install.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/static/install.sh b/site/static/install.sh index 7e4e9d7691dad..6c24761dcbc89 100644 --- a/site/static/install.sh +++ b/site/static/install.sh @@ -188,7 +188,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 @@ -208,7 +208,7 @@ main() { echoh fi - if [ ! has_standalone ]; then + if ! has_standalone; then echoerr "There is no binary for $OS-$ARCH" exit 1 fi @@ -306,7 +306,7 @@ has_standalone() { amd64) return 0 ;; arm64) return 0 ;; armv7) - [ "$(distro)" == "linux" ] + [ "$(distro)" = "linux" ] return ;; *) return 1 ;; From e1b467421456f02ab569e4783c5eb051f5bfe758 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Sat, 11 Jan 2025 01:27:41 +0000 Subject: [PATCH 5/8] hmm --- site/site.go | 14 +++++++++----- site/site_test.go | 12 ------------ 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/site/site.go b/site/site.go index 382b4f1e8b48f..61473d386e24b 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 { - panic("failed to parse install script") + _ = fmt.Errorf("install.sh will be unavailabe: %w", err) } return handler @@ -225,6 +225,10 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { // If requesting the install.sh script, fill in the template with the // appropriate hostname and version info. 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 @@ -552,22 +556,22 @@ type installScriptState struct { func parseInstallScript(files fs.FS, buildInfo codersdk.BuildInfoResponse) ([]byte, error) { scriptFile, err := fs.ReadFile(files, "install.sh") if err != nil { - return []byte{}, err + return nil, err } script, err := template.New("install.sh").Parse(string(scriptFile)) if err != nil { - return []byte{}, err + return nil, err } var buf bytes.Buffer state := installScriptState{Origin: buildInfo.DashboardURL, Version: buildInfo.Version} err = script.Execute(&buf, state) if err != nil { - return []byte{}, err + return nil, err } - return buf.Bytes(), err + return buf.Bytes(), nil } // ExtractOrReadBinFS checks the provided fs for compressed coder binaries and diff --git a/site/site_test.go b/site/site_test.go index dfdc714344295..8bee665a56ae3 100644 --- a/site/site_test.go +++ b/site/site_test.go @@ -41,9 +41,6 @@ func TestInjection(t *testing.T) { "index.html": &fstest.MapFile{ Data: []byte("{{ .User }}"), }, - "install.sh": &fstest.MapFile{ - Data: []byte{}, - }, } binFs := http.FS(fstest.MapFS{}) db := dbmem.New() @@ -102,9 +99,6 @@ func TestInjectionFailureProducesCleanHTML(t *testing.T) { "index.html": &fstest.MapFile{ Data: []byte("{{ .User }}"), }, - "install.sh": &fstest.MapFile{ - Data: []byte{}, - }, } handler := site.New(&site.Options{ BinFS: binFs, @@ -150,9 +144,6 @@ func TestCaching(t *testing.T) { "terminal.html": &fstest.MapFile{ Data: []byte("folderFile"), }, - "install.sh": &fstest.MapFile{ - Data: []byte{}, - }, } binFS := http.FS(fstest.MapFS{}) @@ -363,9 +354,6 @@ func TestServingBin(t *testing.T) { "dashboard.css": &fstest.MapFile{ Data: []byte("dashboard-css-bytes"), }, - "install.sh": &fstest.MapFile{ - Data: []byte{}, - }, } sampleBinFSCorrupted := sampleBinFS() From 85c6b1714e483ef97bc4247dc72ddb618692db25 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Sat, 11 Jan 2025 01:33:22 +0000 Subject: [PATCH 6/8] sure --- site/site.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/site.go b/site/site.go index 61473d386e24b..2f7b809121784 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 { - _ = fmt.Errorf("install.sh will be unavailabe: %w", err) + _ = xerrors.Errorf("install.sh will be unavailable: %w", err) } return handler From 1a6b15bc7e255c64d67471c4695a59fea371963a Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Sat, 11 Jan 2025 01:38:53 +0000 Subject: [PATCH 7/8] boooook --- .../pages/CliInstallPage/CliInstallPageView.stories.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/site/src/pages/CliInstallPage/CliInstallPageView.stories.tsx b/site/src/pages/CliInstallPage/CliInstallPageView.stories.tsx index 425d3306dce89..1140cd88c3c3e 100644 --- a/site/src/pages/CliInstallPage/CliInstallPageView.stories.tsx +++ b/site/src/pages/CliInstallPage/CliInstallPageView.stories.tsx @@ -2,11 +2,8 @@ import type { Meta, StoryObj } from "@storybook/react"; import { CliInstallPageView } from "./CliInstallPageView"; const meta: Meta = { - title: "pages/CliAuthPage", + title: "pages/CliInstallPage", component: CliInstallPageView, - args: { - sessionToken: "some-session-token", - }, }; export default meta; @@ -14,4 +11,4 @@ type Story = StoryObj; const Example: Story = {}; -export { Example as CliAuthPage }; +export { Example as CliInstallPage }; From 95e8458299b955f82bfb4d2199676b50228788bb Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 14 Jan 2025 23:28:00 +0000 Subject: [PATCH 8/8] tweak design --- .../CliInstallPage/CliInstallPageView.tsx | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/site/src/pages/CliInstallPage/CliInstallPageView.tsx b/site/src/pages/CliInstallPage/CliInstallPageView.tsx index 1c349c8bb8d21..a5ec484430228 100644 --- a/site/src/pages/CliInstallPage/CliInstallPageView.tsx +++ b/site/src/pages/CliInstallPage/CliInstallPageView.tsx @@ -1,6 +1,5 @@ import type { Interpolation, Theme } from "@emotion/react"; import { CodeExample } from "components/CodeExample/CodeExample"; -import { SignInLayout } from "components/SignInLayout/SignInLayout"; import { Welcome } from "components/Welcome/Welcome"; import type { FC } from "react"; import { Link as RouterLink } from "react-router-dom"; @@ -9,8 +8,8 @@ export const CliInstallPageView: FC = () => { const origin = location.origin; return ( - - Install the Coder CLI +
+ Install the Coder CLI

Copy the command below and{" "} @@ -18,6 +17,8 @@ export const CliInstallPageView: FC = () => {

@@ -27,20 +28,31 @@ export const CliInstallPageView: FC = () => { Go to workspaces
-
+
+ {"\u00a9"} {new Date().getFullYear()} Coder Technologies, Inc. +
+ ); }; const styles = { + container: { + flex: 1, + height: "-webkit-fill-available", + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + width: 480, + margin: "auto", + }, + instructions: (theme) => ({ fontSize: 16, color: theme.palette.text.secondary, paddingBottom: 8, textAlign: "center", lineHeight: 1.4, - - // Have to undo styling side effects from component - marginTop: -24, }), backLink: (theme) => ({ @@ -57,4 +69,10 @@ const styles = { textDecoration: "none", }, }), + + copyright: (theme) => ({ + fontSize: 12, + color: theme.palette.text.secondary, + marginTop: 24, + }), } satisfies Record>;