#!/usr/bin/env sh # bin/install.sh — install userland devops CLIs into ~/.local/bin. # # Every tool is a self-contained release binary downloaded from upstream and # dropped into ~/.local/bin (no sudo, nothing outside $HOME), so it persists # across Kasm redeploys. Idempotent: each tool records its version under # ~/.local/bin/.versions and is skipped when the pinned version is already # installed. To add a tool, pin a version, add an install_ function, # and call it at the bottom. # # Ansible is not a single binary; it is handled separately in ansible/. set -eu SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" REPO_ROOT="$(dirname -- "$SCRIPT_DIR")" # shellcheck source=../lib/fetch.sh . "$REPO_ROOT/lib/fetch.sh" ARCH="$(fetch_arch)" # x86_64 / aarch64 DEB="$(fetch_arch_deb)" # amd64 / arm64 # docker_full_available — true only if a non-rootless (full) Docker daemon is # up and reachable. kind needs full Docker: we could not get it working against # rootless Docker on Kasm, so it is only worth installing where a rootful # daemon answers. SecurityOptions lists name=rootless under rootless mode. docker_full_available() { command -v docker >/dev/null 2>&1 || return 1 _secopts="$(docker info -f '{{println .SecurityOptions}}' 2>/dev/null)" || return 1 [ -n "$_secopts" ] || return 1 ! printf '%s' "$_secopts" | grep -q 'name=rootless' } # jq — command-line JSON processor (single static binary). install_jq() { ver=1.8.2 have_version jq "$ver" && { fsay "jq $ver already installed."; return 0; } install_raw_bin \ "https://github.com/jqlang/jq/releases/download/jq-$ver/jq-linux-$DEB" jq mark_version jq "$ver" } # kubectl — Kubernetes CLI (single static binary from dl.k8s.io). install_kubectl() { ver=v1.36.2 have_version kubectl "$ver" && { fsay "kubectl $ver already installed."; return 0; } install_raw_bin \ "https://dl.k8s.io/release/$ver/bin/linux/$DEB/kubectl" kubectl mark_version kubectl "$ver" } # kubelogin — kubectl OIDC plugin, so omnictl-generated kubeconfigs can # authenticate (they exec `kubectl oidc-login get-token`). Installed as # kubectl-oidc_login, the name kubectl's plugin lookup expects. install_oidc_login() { ver=v1.36.4 have_version kubectl-oidc_login "$ver" && { fsay "kubectl-oidc_login $ver already installed."; return 0; } install_zip_bin \ "https://github.com/int128/kubelogin/releases/download/$ver/kubelogin_linux_$DEB.zip" \ kubelogin kubectl-oidc_login mark_version kubectl-oidc_login "$ver" } # helm — Kubernetes package manager (from get.helm.sh). install_helm() { ver=v4.2.2 have_version helm "$ver" && { fsay "helm $ver already installed."; return 0; } install_tarball_bin \ "https://get.helm.sh/helm-$ver-linux-$DEB.tar.gz" \ "linux-$DEB/helm" helm mark_version helm "$ver" } # k9s — terminal UI for Kubernetes clusters. Asset uses capital "Linux". install_k9s() { ver=v0.51.0 have_version k9s "$ver" && { fsay "k9s $ver already installed."; return 0; } install_tarball_bin \ "https://github.com/derailed/k9s/releases/download/$ver/k9s_Linux_$DEB.tar.gz" \ k9s k9s mark_version k9s "$ver" } # kind — run local Kubernetes clusters in Docker (single static binary). Gated # on full (non-rootless) Docker at the call site: kind could not be made to # work against rootless Docker on Kasm, so it is skipped there. install_kind() { ver=v0.32.0 have_version kind "$ver" && { fsay "kind $ver already installed."; return 0; } install_raw_bin \ "https://github.com/kubernetes-sigs/kind/releases/download/$ver/kind-linux-$DEB" kind mark_version kind "$ver" } # kubectx / kubens — fast context and namespace switching. Uses x86_64 token. install_kubectx() { ver=v0.11.0 have_version kubectx "$ver" && { fsay "kubectx $ver already installed."; return 0; } install_tarball_bin \ "https://github.com/ahmetb/kubectx/releases/download/$ver/kubectx_${ver}_linux_$ARCH.tar.gz" \ kubectx kubectx mark_version kubectx "$ver" } install_kubens() { ver=v0.11.0 have_version kubens "$ver" && { fsay "kubens $ver already installed."; return 0; } install_tarball_bin \ "https://github.com/ahmetb/kubectx/releases/download/$ver/kubens_${ver}_linux_$ARCH.tar.gz" \ kubens kubens mark_version kubens "$ver" } # stern — multi-pod log tailing. Release tag is vX but asset drops the v. install_stern() { ver=1.34.0 have_version stern "$ver" && { fsay "stern $ver already installed."; return 0; } install_tarball_bin \ "https://github.com/stern/stern/releases/download/v$ver/stern_${ver}_linux_$DEB.tar.gz" \ stern stern mark_version stern "$ver" } # yq — YAML/JSON/XML processor (single static binary). install_yq() { ver=v4.53.3 have_version yq "$ver" && { fsay "yq $ver already installed."; return 0; } install_raw_bin \ "https://github.com/mikefarah/yq/releases/download/$ver/yq_linux_$DEB" yq mark_version yq "$ver" } # OpenTofu — open-source Terraform. Asset drops the leading v. install_tofu() { ver=1.12.3 have_version tofu "$ver" && { fsay "tofu $ver already installed."; return 0; } install_tarball_bin \ "https://github.com/opentofu/opentofu/releases/download/v$ver/tofu_${ver}_linux_$DEB.tar.gz" \ tofu tofu mark_version tofu "$ver" } # op — 1Password CLI, used for secrets storage/retrieval (op read, op run, # op inject). Single static binary shipped as a zip. Sign in with # `op signin` or a service-account token (OP_SERVICE_ACCOUNT_TOKEN) — # credentials are per-box and never live in this repo. install_1p() { ver=2.34.1 have_version op "$ver" && { fsay "op $ver already installed."; return 0; } install_zip_bin \ "https://cache.agilebits.com/dist/1P/op2/pkg/v$ver/op_linux_${DEB}_v$ver.zip" \ op op mark_version op "$ver" } # opencode — terminal AI coding agent. Points at the covert-gpt LiteLLM # gateway via mcp-config/opencode.gateway.json (synced by mcp-config/). # Release assets use x64/arm64 tokens rather than amd64/x86_64. install_opencode() { ver=v1.17.18 have_version opencode "$ver" && { fsay "opencode $ver already installed."; return 0; } case "$DEB" in amd64) _oc_arch=x64 ;; *) _oc_arch=arm64 ;; esac install_tarball_bin \ "https://github.com/sst/opencode/releases/download/$ver/opencode-linux-$_oc_arch.tar.gz" \ opencode opencode mark_version opencode "$ver" } # zarf — a DevSecOps tool for Offline and Airgapped Kubernetes (single static binary). install_zarf() { ver=v0.81.0 have_version zarf "$ver" && { fsay "zarf $ver already installed."; return 0; } case "$DEB" in amd64) _zarf_arch=amd64 ;; *) _zarf_arch=arm64 ;; esac install_raw_bin \ "https://github.com/zarf-dev/zarf/releases/download/$ver/zarf_${ver}_Linux_$_zarf_arch" zarf mark_version zarf "$ver" } # task — go-task, the Taskfile.yml task runner. Binary is at the archive root. install_task() { ver=v3.52.0 have_version task "$ver" && { fsay "task $ver already installed."; return 0; } install_tarball_bin \ "https://github.com/go-task/task/releases/download/$ver/task_linux_$DEB.tar.gz" \ task task mark_version task "$ver" } # packer — infrastructure as code build tool (single static binary). install_packer() { ver=1.15.4 have_version packer "$ver" && { fsay "packer $ver already installed."; return 0; } case "$(fetch_arch)" in x86_64) _packer_arch=amd64 ;; aarch64) _packer_arch=arm64 ;; *) fsay "packer: unsupported arch $(uname -m)"; return 1 ;; esac install_zip_bin \ "https://releases.hashicorp.com/packer/$ver/packer_${ver}_linux_$_packer_arch.zip" \ packer packer mark_version packer "$ver" } # terragrunt — thin wrapper around tofu/terraform (single static binary). install_terragrunt() { ver=v1.1.0 have_version terragrunt "$ver" && { fsay "terragrunt $ver already installed."; return 0; } install_raw_bin \ "https://github.com/gruntwork-io/terragrunt/releases/download/$ver/terragrunt_linux_$DEB" \ terragrunt mark_version terragrunt "$ver" } # coder — CLI for Coder workspaces (coder login, coder ssh, coder config-ssh). # Release tag is vX but asset drops the v; binary is at the archive root. install_coder() { ver=2.36.5 have_version coder "$ver" && { fsay "coder $ver already installed."; return 0; } install_tarball_bin \ "https://github.com/coder/coder/releases/download/v$ver/coder_${ver}_linux_$DEB.tar.gz" \ coder coder mark_version coder "$ver" } # try — run one tool's installer; on failure, warn and keep going. # # WHY: these installs used to be bare calls, so under `set -eu` the FIRST # failure killed this script -- and its non-zero exit then aborted bootstrap.sh # itself, so every later module (node/, ansible/) silently never ran. A single # tool whose release asset moved could leave a box with no opencode, no npx and # no python3. One optional CLI is not worth the whole bootstrap. # # The subshell re-enables `set -e` inside itself (it is disabled for a command # being tested by `if`), so a failed download still aborts that tool's function # before mark_version stamps a version that was never installed. FAILED="" try() { if ( set -e; "$1" ); then return 0; fi FAILED="$FAILED ${1#install_}" fsay "WARNING: ${1#install_} failed to install; continuing." } try install_jq try install_kubectl try install_oidc_login try install_helm try install_k9s if docker_full_available; then try install_kind else fsay "Skipping kind: full (non-rootless) Docker not available." fi try install_kubectx try install_kubens try install_stern try install_yq try install_tofu try install_task try install_terragrunt try install_1p try install_opencode try install_zarf try install_packer try install_coder if [ -n "$FAILED" ]; then fsay "" fsay "WARNING: these tools did NOT install:$FAILED" fsay "Everything else is in $BIN_DIR. Fix the cause and re-run bin/install.sh." else fsay "Done. Tools in $BIN_DIR (added to PATH by zsh/zshrc)." fi