#!/bin/sh # sn-install - install the sn CLI from the platform container registry. # # Needs only `az` (logged in), `curl` and a sha256 tool. No Go, no Git LFS, no # clone of this repo, and no oras or docker: the registry API is plain HTTPS, # and an AAD token traded for a registry token is the only credential involved. # Safe to rerun; it always installs whatever tools/sn:latest currently points at. # # ./bin/sn-install install to /usr/local/bin # SN_INSTALL_DIR=~/.local/bin ./bin/sn-install install without sudo # # Your identity needs AcrPull on the registry, which infra-operators grants. # That is a data-plane role, so nothing here touches Azure Resource Manager - # an operator with no control-plane access to the shared subscription (not even # Reader, and so no `shared` entry in `az account list`) can still install. # Once installed, `sn upgrade` does the same job without this script. set -eu REGISTRY=acrsensenplatform.azurecr.io REPO=tools/sn MANIFEST_TYPE=application/vnd.oci.image.manifest.v1+json INSTALL_DIR=${SN_INSTALL_DIR:-/usr/local/bin} die() { echo "sn-install: $*" >&2; exit 1; } need() { command -v "$1" >/dev/null 2>&1 || die "$1 is required but not installed"; } # There is no such thing as a binary that runs on both amd64 and arm64 Linux, # so the registry holds one per platform and this picks. The names are Go's # GOOS/GOARCH, which is what tagged them. case $(uname -s) in Linux) os=linux ;; Darwin) os=darwin ;; *) die "unsupported OS $(uname -s) - sn is published for Linux and macOS" ;; esac case $(uname -m) in x86_64 | amd64) arch=amd64 ;; aarch64 | arm64) arch=arm64 ;; *) die "unsupported architecture $(uname -m) - sn is published for amd64 and arm64" ;; esac TAG=latest-$os-$arch need az need curl if command -v sha256sum >/dev/null 2>&1; then checksum() { sha256sum "$1" | cut -d' ' -f1; } elif command -v shasum >/dev/null 2>&1; then checksum() { shasum -a 256 "$1" | cut -d' ' -f1; } else die "need sha256sum or shasum to verify the download" fi echo "authenticating to $REGISTRY" # `az acr login` would resolve the registry through ARM first, which fails for # an identity holding only the data-plane pull role. Mint the ARM-audience AAD # token ourselves and trade it at the registry's own exchange endpoint instead: # same credential, no control-plane call. aad_token=$(az account get-access-token --resource https://management.azure.com/ \ --output tsv --query accessToken) || die "no Azure token - run 'az login' (add --allow-no-subscriptions if you have no subscriptions)" refresh_token=$(curl -sS -X POST "https://$REGISTRY/oauth2/exchange" \ -d "grant_type=access_token&service=$REGISTRY&access_token=$aad_token" | sed -n 's/.*"refresh_token":"\([^"]*\)".*/\1/p') [ -n "$refresh_token" ] || die "$REGISTRY rejected your Azure token - is your identity in infra-operators?" # ACR rejects Basic auth on /v2, so the refresh token has to be exchanged for # a bearer token. Scope it to pulling this one repository and nothing else. access_token=$(curl -sS -X POST "https://$REGISTRY/oauth2/token" \ -d "grant_type=refresh_token&service=$REGISTRY&scope=repository:$REPO:pull&refresh_token=$refresh_token" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') [ -n "$access_token" ] || die "no pull token for $REPO - is your identity in infra-operators?" fetch_manifest() { curl -sS -H "Authorization: Bearer $access_token" -H "Accept: $MANIFEST_TYPE" \ "https://$REGISTRY/v2/$REPO/manifests/$1" } echo "resolving $REPO:$TAG" manifest=$(fetch_manifest "$TAG") resolved=$TAG if ! echo "$manifest" | grep -q '"layers"' && [ "$os/$arch" = "linux/amd64" ]; then # Copies published before platform tags existed live under bare `latest`. manifest=$(fetch_manifest latest) resolved=latest fi echo "$manifest" | grep -q '"layers"' || die "nothing published for $os/$arch (looked for $REPO:$TAG)" # `sn release` records the platform it built for. Checking it turns "cannot # execute binary file" - the symptom when this script itself is out of date, # since the Worker that serves it embeds a copy - into something diagnosable. built_for=$(echo "$manifest" | sed -n 's/.*"ai.sensen.sn.platform":"\([^"]*\)".*/\1/p') if [ -n "$built_for" ] && [ "$built_for" != "$os/$arch" ]; then die "$REPO:$resolved holds a $built_for build, but this is $os/$arch. This installer is out of date: re-fetch it, or if you piped it from a URL, the endpoint needs republishing (terraform apply subscriptions/dev-k8s)." fi # The binary is the manifest's single layer, and a layer digest is the sha256 # of its content, so the manifest doubles as the checksum. digest=$(echo "$manifest" | sed 's/.*"layers"//' | grep -o 'sha256:[0-9a-f]\{64\}' | head -1) [ -n "$digest" ] || die "no layer digest in the $REPO:$resolved manifest" tmp=$(mktemp) trap 'rm -f "$tmp"' EXIT echo "downloading ${digest#sha256:}" curl -fsSL -H "Authorization: Bearer $access_token" \ "https://$REGISTRY/v2/$REPO/blobs/$digest" -o "$tmp" || die "download failed" got=$(checksum "$tmp") [ "sha256:$got" = "$digest" ] || die "checksum mismatch: got $got, expected ${digest#sha256:}" mkdir -p "$INSTALL_DIR" 2>/dev/null || true if [ -w "$INSTALL_DIR" ]; then install -m 0755 "$tmp" "$INSTALL_DIR/sn" elif command -v sudo >/dev/null 2>&1; then echo "installing to $INSTALL_DIR (needs sudo)" sudo install -m 0755 "$tmp" "$INSTALL_DIR/sn" else die "$INSTALL_DIR is not writable and sudo is unavailable - set SN_INSTALL_DIR" fi echo "$INSTALL_DIR/sn" version case ":$PATH:" in *":$INSTALL_DIR:"*) ;; *) echo "note: $INSTALL_DIR is not on your PATH" ;; esac