diff --git a/README.md b/README.md index 64b1c29..ee54887 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,18 @@ Already have a clone? Just run it directly: - `git/` — sets git `user.name`/`user.email` and configures `credential.helper store` (plaintext `~/.git-credentials`, 0600) for HTTPS auth. -- `bin/` — installs userland CLI tools (ripgrep, fd, bat, fzf, jq) as - self-contained release binaries into `~/.local/bin`. No sudo, nothing +- `bin/` — installs userland devops CLIs (jq, kubectl, helm, k9s, + kubectx/kubens, stern, yq, OpenTofu, terragrunt) as self-contained, + statically linked release binaries into `~/.local/bin`. No sudo, nothing outside `$HOME`, so they persist across Kasm redeploys with no reinstall. - Idempotent: pinned versions already present are skipped. `zsh/zshrc` puts - `~/.local/bin` on `PATH`. + Idempotent: each tool records its version under `~/.local/bin/.versions` + and is skipped when the pinned version is already present. `zsh/zshrc` + puts `~/.local/bin` on `PATH`. +- `ansible/` — installs Ansible into a virtualenv at `~/.local/opt/ansible` + (it's Python, not a single binary) and symlinks its CLIs into + `~/.local/bin`. Persists across redeploys; only rebuilt when the venv is + missing or broken. Building it may install `python3`/`python3-venv` via + the system package manager (the one place this module uses sudo). - `mcp-config/` — shared MCP gateway config sync for laptops (LiteLLM / opencode). Independent of `bootstrap.sh`; see its own README. diff --git a/ansible/install.sh b/ansible/install.sh new file mode 100755 index 0000000..22af17d --- /dev/null +++ b/ansible/install.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env sh +# ansible/install.sh — install Ansible into a userland virtualenv and expose +# its CLIs on PATH. Ansible is Python, not a single binary, so it can't live +# in bin/ alongside the downloaded release binaries. +# +# The venv lives at ~/.local/opt/ansible and its CLIs are symlinked into +# ~/.local/bin. Both are under $HOME, so they persist across Kasm redeploys; +# the venv is only rebuilt when missing. Building it needs python3 + venv, +# which may require a system package on a fresh box (the one place this +# module uses sudo, via pkg_install). +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +REPO_ROOT="$(dirname -- "$SCRIPT_DIR")" + +# shellcheck source=../lib/os-detect.sh +. "$REPO_ROOT/lib/os-detect.sh" + +say() { printf '%s\n' "$*" >&2; } + +VENV="$HOME/.local/opt/ansible" +BIN_DIR="$HOME/.local/bin" +ANSIBLE_CLIS="ansible ansible-playbook ansible-galaxy ansible-vault ansible-config ansible-doc ansible-inventory ansible-console ansible-pull" + +# Verify it actually runs, not just that the file exists: a Kasm redeploy can +# wipe the system python3 the venv links to, leaving a present-but-broken +# venv that must be rebuilt. +if "$VENV/bin/ansible" --version >/dev/null 2>&1; then + say "ansible venv already present at $VENV." +else + rm -rf "$VENV" + command -v python3 >/dev/null 2>&1 || pkg_install python3 + say "Creating ansible venv at $VENV..." + if ! python3 -m venv "$VENV" 2>/dev/null; then + # Debian splits venv/ensurepip into python3-venv; install it and retry. + say "python venv module missing; installing it..." + case "$(detect_pkg_family)" in + debian) pkg_install python3-venv python3-pip ;; + rhel) pkg_install python3 python3-pip ;; + *) say "install python3 venv support manually"; exit 1 ;; + esac + rm -rf "$VENV" + python3 -m venv "$VENV" + fi + "$VENV/bin/pip" install --quiet --upgrade pip + say "Installing ansible (this pulls in a lot of collections)..." + "$VENV/bin/pip" install --quiet ansible + say "Installed ansible into $VENV." +fi + +# Symlink the CLIs into ~/.local/bin (which is on PATH via zsh/zshrc). +mkdir -p "$BIN_DIR" +for cli in $ANSIBLE_CLIS; do + if [ -x "$VENV/bin/$cli" ]; then + ln -sf "$VENV/bin/$cli" "$BIN_DIR/$cli" + fi +done +say "Linked ansible CLIs into $BIN_DIR." diff --git a/bin/install.sh b/bin/install.sh index 70814b5..0cf5357 100755 --- a/bin/install.sh +++ b/bin/install.sh @@ -1,11 +1,14 @@ #!/usr/bin/env sh -# bin/install.sh — install userland CLI tools into ~/.local/bin. +# 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 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. +# 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)" @@ -14,70 +17,113 @@ REPO_ROOT="$(dirname -- "$SCRIPT_DIR")" # shellcheck source=../lib/fetch.sh . "$REPO_ROOT/lib/fetch.sh" -ARCH="$(fetch_arch)" - -# ripgrep — fast recursive search. x86_64 ships musl (static); aarch64 gnu. -install_ripgrep() { - ver=15.1.0 - have_version rg "$ver" && { fsay "ripgrep $ver already installed."; return 0; } - case "$ARCH" in - x86_64) triple=x86_64-unknown-linux-musl ;; - aarch64) triple=aarch64-unknown-linux-gnu ;; - esac - install_tarball_bin \ - "https://github.com/BurntSushi/ripgrep/releases/download/$ver/ripgrep-$ver-$triple.tar.gz" \ - "ripgrep-$ver-$triple/rg" rg -} - -# fd — fast, user-friendly find. musl (static) on both arches. -install_fd() { - ver=10.4.2 - have_version fd "$ver" && { fsay "fd $ver already installed."; return 0; } - triple="$ARCH-unknown-linux-musl" - install_tarball_bin \ - "https://github.com/sharkdp/fd/releases/download/v$ver/fd-v$ver-$triple.tar.gz" \ - "fd-v$ver-$triple/fd" fd -} - -# bat — cat with syntax highlighting. musl (static) on both arches. -install_bat() { - ver=0.26.1 - have_version bat "$ver" && { fsay "bat $ver already installed."; return 0; } - triple="$ARCH-unknown-linux-musl" - install_tarball_bin \ - "https://github.com/sharkdp/bat/releases/download/v$ver/bat-v$ver-$triple.tar.gz" \ - "bat-v$ver-$triple/bat" bat -} - -# fzf — general-purpose fuzzy finder (static Go binary). -install_fzf() { - ver=0.74.0 - have_version fzf "$ver" && { fsay "fzf $ver already installed."; return 0; } - case "$ARCH" in - x86_64) a=amd64 ;; - aarch64) a=arm64 ;; - esac - install_tarball_bin \ - "https://github.com/junegunn/fzf/releases/download/v$ver/fzf-$ver-linux_$a.tar.gz" \ - fzf fzf -} +ARCH="$(fetch_arch)" # x86_64 / aarch64 +DEB="$(fetch_arch_deb)" # amd64 / arm64 # 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; } - case "$ARCH" in - x86_64) a=amd64 ;; - aarch64) a=arm64 ;; - esac install_raw_bin \ - "https://github.com/jqlang/jq/releases/download/jq-$ver/jq-linux-$a" jq + "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" +} + +# 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" +} + +# 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" +} + +# 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" } -install_ripgrep -install_fd -install_bat -install_fzf install_jq +install_kubectl +install_helm +install_k9s +install_kubectx +install_kubens +install_stern +install_yq +install_tofu +install_terragrunt fsay "Done. Tools in $BIN_DIR (added to PATH by zsh/zshrc)." diff --git a/bootstrap.sh b/bootstrap.sh index e1f3670..337ac13 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -32,3 +32,4 @@ fi "$SCRIPT_DIR/zsh/install.sh" "$SCRIPT_DIR/git/install.sh" "$SCRIPT_DIR/bin/install.sh" +"$SCRIPT_DIR/ansible/install.sh" diff --git a/lib/fetch.sh b/lib/fetch.sh index 6139443..10e50cc 100644 --- a/lib/fetch.sh +++ b/lib/fetch.sh @@ -3,18 +3,20 @@ # # Everything lands under $HOME, so installed tools survive Kasm redeploys # (which wipe the root filesystem but keep $HOME) with no reinstall needed. -# No sudo, no system packages. Prefer statically linked (musl) release -# binaries so they keep working even after the root fs — and its shared -# libraries — is reset. +# No sudo, no system packages. Prefer statically linked release binaries so +# they keep working even after the root fs — and its shared libraries — is +# reset. # -# Source this (callers already run `set -eu`), then use fetch_arch, -# have_version, install_tarball_bin, and install_raw_bin. +# Source this (callers already run `set -eu`), then use fetch_arch / +# fetch_arch_deb, have_version + mark_version, install_tarball_bin, and +# install_raw_bin. BIN_DIR="${BIN_DIR:-$HOME/.local/bin}" +VERSION_DIR="$BIN_DIR/.versions" fsay() { printf '%s\n' "$*" >&2; } -# Normalize `uname -m` to the canonical arch tokens used below. +# Normalize `uname -m` to the tokens rust-style release names use. fetch_arch() { case "$(uname -m)" in x86_64|amd64) printf 'x86_64\n' ;; @@ -23,6 +25,15 @@ fetch_arch() { esac } +# Same, but in the Go/Debian token style most devops tools use. +fetch_arch_deb() { + case "$(uname -m)" in + x86_64|amd64) printf 'amd64\n' ;; + aarch64|arm64) printf 'arm64\n' ;; + *) fsay "fetch: unsupported arch $(uname -m)"; return 1 ;; + esac +} + # Download to using whatever downloader is available. _fetch_dl() { if command -v curl >/dev/null 2>&1; then @@ -34,12 +45,20 @@ _fetch_dl() { fi } -# have_version — true if ~/.local/bin/ exists -# and reports the given version, so bootstrap can skip a re-download. +# have_version — true if is installed and was +# stamped at , so bootstrap can skip a re-download. Versions are +# recorded in ~/.local/bin/.versions rather than parsed from each tool's +# `--version` output, which varies wildly (kubectl/helm/k9s have no uniform +# flag). have_version() { - _bin="$BIN_DIR/$1" - [ -x "$_bin" ] || return 1 - "$_bin" --version 2>/dev/null | grep -qF "$2" + [ -x "$BIN_DIR/$1" ] && [ -f "$VERSION_DIR/$1" ] && + [ "$(cat "$VERSION_DIR/$1")" = "$2" ] +} + +# mark_version — record the installed version. +mark_version() { + mkdir -p "$VERSION_DIR" + printf '%s\n' "$2" >"$VERSION_DIR/$1" } # install_tarball_bin — download a