#!/bin/sh # Installs tetrate, the command for Tetrate products, from a release of tetrateio/cli. # # curl -fsSL https://get.tetrate.io | GITHUB_TOKEN=$(gh auth token) sh # curl -fsSL https://get.tetrate.io | GITHUB_TOKEN=$(gh auth token) sh -s -- --version v0.1.0-alpha.1 # # Run it with --help for the options, the version rule and the token sources. # # Generated by tools/site in tetrateio/cli. Do not edit it here. set -eu repo=tetrateio/cli api=https://api.github.com identity='^https://github\.com/tetrateio/cli/\.github/workflows/release\.yml@refs/tags/v' issuer=https://token.actions.githubusercontent.com # The tests set these three to run on any host. uname_s=${TETRATE_TEST_UNAME_S:-$(uname -s)} uname_m=${TETRATE_TEST_UNAME_M:-$(uname -m)} arm64_cpu=${TETRATE_TEST_ARM64:-} fail() { echo "install.sh: $*" >&2 exit 1 } usage() { cat <<'EOF' Usage: curl -fsSL https://get.tetrate.io | sh -s -- [--version vX] [--dir DIR] Installs the tetrate binary from the GitHub releases of tetrateio/cli. Options: --version vX install the release with this tag, such as v0.1.0-alpha.1. Without it, install the newest release. Prereleases count only while no other release exists. --dir DIR install into DIR (default $HOME/.local/bin) -h, --help show this help Environment: TETRATE_INSTALL_DIR the install directory, when --dir is not given TETRATE_GITHUB_TOKEN_FILE a file that holds the GitHub token, read first GITHUB_TOKEN the token, read when the file is not set GH_TOKEN the token, read when both of the above are unset (none of them) the output of "gh auth token", when gh is installed tetrateio/cli is private, so a GitHub token that can read it is required: a fine-grained token with Contents: read on tetrateio/cli, or a classic token with repo. The script never prints the token and never puts it on a command line. Platforms: darwin-arm64, linux-amd64, linux-arm64. The script checks the sha256 of the binary against checksums.txt of the release. When cosign is on PATH it also verifies the sigstore bundle of the binary. It refuses to install when either check fails. EOF } version= dir=${TETRATE_INSTALL_DIR:-} while [ "$#" -gt 0 ]; do case $1 in -h | --help) usage exit 0 ;; --version) [ "$#" -ge 2 ] || fail "--version needs a value, such as v0.1.0" version=$2 shift 2 ;; --version=*) version=${1#--version=} shift ;; --dir) [ "$#" -ge 2 ] || fail "--dir needs a directory" dir=$2 shift 2 ;; --dir=*) dir=${1#--dir=} shift ;; *) fail "unknown argument $1. Run install.sh --help" ;; esac done case $version in '' | v*) ;; *) version=v$version ;; esac case $version in '' | v[0-9]*) ;; *) fail "version $version is not a release tag such as v0.1.0" ;; esac case $version in *[!A-Za-z0-9._+-]*) fail "version $version has a character that a release tag does not have" ;; esac if [ -z "$dir" ]; then [ -n "${HOME:-}" ] || fail "HOME is not set. Pass --dir or set TETRATE_INSTALL_DIR." dir=$HOME/.local/bin fi case $uname_s in Darwin) os=darwin ;; Linux) os=linux ;; *) os=$uname_s ;; esac case $uname_m in x86_64 | amd64) arch=amd64 ;; aarch64 | arm64) arch=arm64 ;; *) arch=$uname_m ;; esac # A shell that runs under Rosetta on an Apple silicon Mac reports x86_64. if [ "$os" = darwin ] && [ "${arm64_cpu:-$(sysctl -n hw.optional.arm64 2>/dev/null || true)}" = 1 ]; then arch=arm64 fi case $os/$arch in darwin/arm64 | linux/amd64 | linux/arm64) ;; *) fail "tetrate has no release for $os/$arch. Supported: darwin-arm64, linux-amd64, linux-arm64." ;; esac for tool in curl awk sed tr grep mktemp cp chmod mv; do command -v "$tool" >/dev/null 2>&1 || fail "$tool is required." done tmp= new= cleanup() { [ -z "$tmp" ] || rm -rf "$tmp" [ -z "$new" ] || rm -f "$new" } trap cleanup EXIT trap 'exit 1' HUP INT TERM tmp=$(mktemp -d) || fail "cannot create a temporary directory" access="The token needs read access to $repo: a fine-grained token with Contents: read on $repo, or a classic token with repo." # The token: the file that TETRATE_GITHUB_TOKEN_FILE names, then # GITHUB_TOKEN, then GH_TOKEN, then gh auth token. The script never prints it. token= if [ -n "${TETRATE_GITHUB_TOKEN_FILE:-}" ]; then if [ ! -f "$TETRATE_GITHUB_TOKEN_FILE" ] || [ ! -r "$TETRATE_GITHUB_TOKEN_FILE" ]; then fail "TETRATE_GITHUB_TOKEN_FILE=$TETRATE_GITHUB_TOKEN_FILE is not a readable file." fi token=$(tr -d ' \t\r\n' <"$TETRATE_GITHUB_TOKEN_FILE") [ -n "$token" ] || fail "TETRATE_GITHUB_TOKEN_FILE=$TETRATE_GITHUB_TOKEN_FILE is empty." elif [ -n "${GITHUB_TOKEN:-}" ]; then token=$GITHUB_TOKEN elif [ -n "${GH_TOKEN:-}" ]; then token=$GH_TOKEN elif command -v gh >/dev/null 2>&1; then token=$(gh auth token /dev/null | tr -d ' \t\r\n' || true) fi [ -n "$token" ] || fail "$repo is private and no GitHub token was found. Set TETRATE_GITHUB_TOKEN_FILE to a file that holds a token, set GITHUB_TOKEN or GH_TOKEN, or run gh auth login. $access" # The token goes into a curl configuration line. A character outside the # set of a GitHub token could break that line. case $token in *[!A-Za-z0-9_.~+/=-]*) fail "the GitHub token has a character that a token does not have. Check where it comes from." ;; esac # github GETs an API path or an asset URL with the token and writes the body # to $3. It prints the HTTP status. The token goes to curl on standard input, # as a configuration line, so no command line holds it. github() { printf 'header = "Authorization: Bearer %s"\n' "$token" | curl -fsSL --proto '=https' --proto-redir '=https' --connect-timeout 20 --max-time 600 \ -o "$3" -w '%{http_code}' -K - \ -H "Accept: $2" -H "X-GitHub-Api-Version: 2022-11-28" "$1" } # denied explains an HTTP status of the GitHub API. GitHub answers 404 for a # private repository that the token cannot read. denied() { case $1 in 401) echo "HTTP 401: the token is invalid or expired. $access" ;; 403) echo "HTTP 403: the token cannot read $repo, or the GitHub API rate limit is used up. Retry later if the token worked before. $access" ;; 404) echo "HTTP 404: the token cannot read $repo, or there is no such release. $access" ;; 429) echo "HTTP 429: the GitHub API rate limit is used up. Retry later." ;; *) echo "HTTP $1 (000 means no HTTP response)" ;; esac } # fields prints one "key value" line per string or literal field of the JSON # on stdin, in document order, and a "}" line where an object starts or ends. # Reading it does not depend on the order of keys. fields() { # shellcheck disable=SC2020 # one newline per character, on purpose tr ',[]' '\n\n\n' | awk '{ gsub(/[{}]/, "\n}\n"); print }' | sed -n \ -e 's/^[[:space:]]*}[[:space:]]*$/}/p' \ -e 's/^[[:space:]]*"\([a-z_]*\)":[[:space:]]*"\{0,1\}\([^"]*\)"\{0,1\}[[:space:]]*$/\1 \2/p' } # releases prints "TAG PRERELEASE" for each published release of $repo, newest # first. It reads every page of 100 releases. releases() { page=1 : >"$tmp/releases.json" while :; do status=$(github "$api/repos/$repo/releases?per_page=100&page=$page" application/vnd.github+json "$tmp/page.json") || fail "cannot list the releases of $repo: $(denied "$status")" cat "$tmp/page.json" >>"$tmp/releases.json" n=$(grep -c '"tag_name"' "$tmp/page.json" || true) [ "$n" -eq 100 ] || break page=$((page + 1)) [ "$page" -le 50 ] || fail "$repo has more than 5000 releases" done fields <"$tmp/releases.json" | awk ' $1 == "}" { if (t != "" && d != "true") print t, p t = ""; d = ""; p = ""; next } $1 == "tag_name" { t = $2 } $1 == "draft" { d = $2 } $1 == "prerelease" { p = $2 } ' } # The release: --version, or the newest published release. A prerelease is # chosen only when no release that is not a prerelease exists. tag=$version if [ -z "$tag" ]; then releases >"$tmp/tags" tag=$(awk '$2 == "false" && $1 ~ /^v[0-9]/ { print $1; exit }' "$tmp/tags") [ -n "$tag" ] || tag=$(awk '$1 ~ /^v[0-9]/ { print $1; exit }' "$tmp/tags") [ -n "$tag" ] || fail "cannot find a release in $repo. Check the repository access, or pass --version." fi asset=tetrate-$tag-$os-$arch bundle=$asset.sigstore.json have_cosign=false if command -v cosign >/dev/null 2>&1; then have_cosign=true fi echo "Installing tetrate $tag for $os/$arch from $repo" status=$(github "$api/repos/$repo/releases/tags/$tag" application/vnd.github+json "$tmp/release.json") || fail "cannot read release $tag of $repo: $(denied "$status")" # The API URL of each wanted asset, from the asset object with its name. fields <"$tmp/release.json" | awk -v a="$asset" -v b="$bundle" -v sig="$have_cosign" ' $1 == "}" { if (n != "" && u != "") print n, u n = ""; u = ""; next } $1 == "url" && $2 ~ /\/releases\/assets\// { u = $2 } $1 == "name" && ($2 == a || $2 == "checksums.txt" || (sig == "true" && $2 == b)) { n = $2 } ' >"$tmp/assets" while read -r name url; do status=$(github "$url" application/octet-stream "$tmp/$name") || fail "cannot download $name of release $tag: $(denied "$status")" done <"$tmp/assets" [ -f "$tmp/$asset" ] || fail "release $tag has no asset $asset" [ -f "$tmp/checksums.txt" ] || fail "release $tag has no checksums.txt" want=$(awk -v a="$asset" '$2 == a || $2 == "*" a { print $1 }' "$tmp/checksums.txt") if command -v sha256sum >/dev/null 2>&1; then got=$(sha256sum "$tmp/$asset" | awk '{ print $1 }') elif command -v shasum >/dev/null 2>&1; then got=$(shasum -a 256 "$tmp/$asset" | awk '{ print $1 }') else fail "sha256sum or shasum is required to check the download" fi [ -n "$want" ] || fail "checksums.txt of release $tag has no line for $asset" [ "$got" = "$want" ] || fail "$asset does not match checksums.txt: got $got, want $want" if $have_cosign; then [ -f "$tmp/$bundle" ] || fail "release $tag has no $bundle, so the signature cannot be checked" if ! cosign verify-blob --bundle "$tmp/$bundle" \ --certificate-identity-regexp "$identity" \ --certificate-oidc-issuer "$issuer" \ "$tmp/$asset" /dev/null 2>"$tmp/cosign.err"; then sed -n '1,3p' "$tmp/cosign.err" >&2 fail "the signature of $asset does not verify with cosign. Nothing was installed." fi echo "Verified the sha256 and the sigstore signature of $asset" else echo "Verified the sha256 of $asset. The signature was not checked: install cosign and run cosign verify-blob with $bundle, as docs/releasing.md of $repo describes." fi mkdir -p "$dir" || fail "cannot create $dir" new=$(mktemp "$dir/.tetrate.XXXXXX") || fail "cannot write to $dir" cp "$tmp/$asset" "$new" || fail "cannot write to $dir" chmod 0755 "$new" mv -f "$new" "$dir/tetrate" || fail "cannot install $dir/tetrate" new= # The binary has no Apple Developer ID signature yet (see #455 in # tetrateio/router), so Gatekeeper blocks it on first run when the file carries # com.apple.quarantine. A browser download, some proxies and some MDM setups # set it. Remove it from the installed file. This goes away when the binary is # notarized. if [ "$os" = darwin ] && command -v xattr >/dev/null 2>&1; then if xattr -d com.apple.quarantine "$dir/tetrate" 2>/dev/null; then echo "Removed the macOS quarantine flag from $dir/tetrate, because the binary is not notarized yet" fi fi echo "Installed $dir/tetrate" case :$PATH: in *:"$dir":*) ;; *) echo "Add it to PATH: export PATH=\"$dir:\$PATH\"" ;; esac "$dir/tetrate" version