11 KiB
ctrl/deps.sh
Purpose and safety
Toolchain installer: detect the host, install a pinned toolchain onto it, then report what it could not do.
It never runs the cluster, never uses sudo or apt, and writes only into $OUT_BIN (default ~/.local/bin). Everything that would touch the host proper — systemd, inotify limits, .wslconfig, docker group — is REPORTED for a human to decide on, never performed. That is what makes it safe to run on a machine that already has a working setup.
Usage
Normally via make deps, or directly:
deps.sh detect # report host facts only, change nothing
deps.sh list # the pinned versions
deps.sh verify [core|dev] # run what is installed and see if it works
deps.sh fetch [core|dev] [--to DIR] # download + verify into DIR
deps.sh install [core|dev] # detect, fetch, install, report
Tiers: core is kubectl + jq (talk to a cluster); dev adds kind and tilt. Default is dev.
Container vs bare host
Runs both inside the installer container and bare on a host. Inside the container, host files are read through $HOST_ROOT (mount / as :ro); bare, it falls back to /.
Host FILES (/etc/..., /mnt/c/...) must be read through the mount. Kernel-level facts (kernel version, meminfo, inotify) are shared with the container, so the container's own view is already the host's.
INVOKED_FROM
Keep the caller's cwd so a relative --to resolves where the user expects, not against ctrl/ once we've moved.
load_config
Pins arrive through load_config like every other setting, not by sourcing versions.env here. That is what lets make standalone freeze them into a one-file installer: configuration has exactly one way in.
mb_of
A /proc/meminfo field in MB, 0 if the field is absent. MEMINFO exists so the tight and does-not-fit branches can be exercised against a real machine's numbers from somewhere else; in normal use it is always /proc/meminfo.
require_amd64
The pins are amd64. Rather than download something that cannot execute and let it fail as "cannot execute binary file: Exec format error", say so here and hand over the commands that produce the right checksums.
pkg_install_cmd
This never runs a package manager. It names one so the reported action is something you can paste, on the distro you are actually on — an apt line on Amazon Linux 2 is a wrong answer dressed up as help.
require_linux
Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and then fails in a pile of confusing ways: no /proc, no docker socket, none of the tooling. Detectable, so name it instead.
detect: memory
In MB. Whole gigabytes lose nearly half a GB on exactly the machines where it matters: 1874 MB available used to print as "1 GB". Facts only — whether that is enough depends on the profile, which check.sh knows and this does not.
detect: overcommit
How the kernel answers an allocation it cannot really satisfy. With 1 it always says yes and settles up later with the OOM killer, so a cluster that starts cleanly can still lose processes afterwards.
detect_wsl: systemd
systemd is off by default in WSL, and the ingress/DNS paths that use a host service need it. Enabling it requires a Windows-side restart, which cannot be issued from inside the distro.
watch_hostile_fs
Not a path check: /mnt is an ordinary mount point and an ext4 disk mounted there is perfectly fine. What matters is the filesystem. The Windows drives arrive as 9p (WSL2) or drvfs (WSL1); network and fuse mounts behave the same way. None of them deliver inotify events, so anything watching files goes quiet without saying why.
detect_libc
tilt is the one binary here that needs a recent glibc. MEASURED, not guessed: tilt 0.37.6 on Amazon Linux 2 (glibc 2.26) fails with
/lib64/libc.so.6: version `GLIBC_2.34' not found (required by .../tilt)
which names a symbol rather than the problem. Amazon Linux 2 is a stock WorkSpaces bundle, so this is the likely case, not an exotic one. Report the version now; verify catches the actual failure after installing.
detect_prereqs
What this script needs to do its own job. Reported here so detect answers "will install work?" instead of leaving you to find out one download in. Amazon Linux 2 ships without tar, which is exactly the surprise this catches.
detect_docker
Reachability of the daemon is the real question, and the CLI is only how we ask it. When this runs inside the installer container, Docker necessarily exists on the host — otherwise nothing would be executing — so a missing CLI in there is an installer packaging bug, not a host problem.
The kind-node count check must be an if, not [ ] && echo: as the last statement in the function the latter returns 1 when the count is zero, and set -e then kills the caller. That is the fresh-machine case — no clusters yet — so the bug only ever shows up where it does most harm.
fetch_tgz: --no-same-owner
Extracting as root would otherwise restore the uid/gid baked into the archive (some ship as uid 1001), leaving a binary the host user does not own.
fix_ownership
The installer runs as root so it can reach the docker socket, which means everything it writes into a mounted volume lands root-owned and unusable from the host. Hand it back to whoever owns the mount point (the host user created that directory before mounting it).
kind writes the kubeconfig as root too; fetch hands that back as well when it's a mounted host directory rather than container-local state.
Tiers (CORE_TOOLS, DEV_TOOLS)
Two tiers, because not every machine should get cluster tooling.
core— kubectl, jq: talk to a cluster someone else runs. Nothing that creates one. Appropriate on a managed or corporate-issued machine where development tools are not wanted by default.dev— core plus kind and tilt: build clusters and hot-reload into them.
The split exists because "install the toolchain" is not one decision: on a managed workspace the right answer is kubectl and nothing else.
No helm: every addon installs with kubectl apply -f <url>, so nothing here has ever invoked it. Add it back the day something actually needs a chart.
ctlptl is dev rather than core for the same reason kind is: core is "talk to a cluster someone else runs", and ctlptl builds them. It earns its place because it is what wires a cluster to a local registry — without one, an unqualified image name resolves to docker.io/library/<name> and there is nothing structural stopping a push there.
docker-compose is dev for the same reason, and is here because the distro docker packages ship the daemon and CLI but frequently not the compose plugin — so docker compose up fails with "unknown command" on an otherwise working Docker, and nothing about that message names the missing piece.
What is already on this machine (pin_of)
A tool already on PATH at its pinned version is left where it is. Without this, install downloads a second copy into OUT_BIN and then reports the first one as shadowed — noise, and wrong, when both are the same version. That is the normal state of any machine someone set up by hand, whatever directory they happened to choose.
reported_version
Each tool spells the version question differently, and kubectl has to be told --client or it goes looking for a server to ask.
version_matches
Matched as a whole version token, so 0.37.6 never matches 10.37.60, with the leading v optional either side: kind says v0.32.0, jq says jq-1.8.2, and tilt says v0.37.6 against a pin of 0.37.6.
Bash's own regex rather than grep, deliberately. grep is not the same program on every machine — some builds reject patterns that others accept — and a failed grep inside a count reads exactly like a zero.
want / DEPS_ONLY
DEPS_ONLY narrows a fetch to the tools it names. Unset means the whole tier, which is what an explicit deps.sh fetch always gets: "download these into DIR" must not quietly skip something because this machine happens to have it. Only install() sets it, to what detect_toolchain found missing or mismatched.
detect_toolchain: compose
compose is the one tool that is normally NOT a binary on PATH. It is a docker CLI plugin, so a machine where docker compose works perfectly has no docker-compose to find — and probing only PATH would report it missing and re-download a copy that is already there. That is the exact noise the version-aware skip exists to prevent, so ask docker instead.
verify_tools
Installing into a directory that sits early in PATH silently replaces whatever the machine was already using — which on a shared or client machine can break unrelated work (kubectl more than one minor away from a cluster is the common one). Say so; never decide it for them.
Downloading a verified binary proves it is the right file, not that this machine can run it. On an old distro tilt fails here, with a linker error about a missing symbol, and finding that out now beats finding out during a first cluster build.
Output is not piped into head. With pipefail set, a tool that prints more than one line gets SIGPIPE when head closes the pipe, and the pipeline reports 141 — so a working kubectl was announced as "does not run here", with its own correct version string as the evidence. The first line is taken afterwards, from the string.
install_compose_plugin
A copy in OUT_BIN only gives you docker-compose. That hyphenated form is the retired v1 spelling; every compose file written in the last few years assumes docker compose, which resolves plugins BY NAME out of a plugin directory. So the binary is fetched like any other and then linked, in your own home — no root, and nothing outside it.
If something else already owns that name — docker-desktop and some distro packages install a real file there — overwriting it would take the plugin away from whatever put it there, so say so and let the user decide.
install
The plugin is linked only when compose was one of the things fetched: linking a binary that is already satisfied elsewhere on PATH would point the plugin at a copy rig did not install.
The "put OUT_BIN on PATH" advice is only worth giving when something actually landed in OUT_BIN. When every tool was satisfied elsewhere, OUT_BIN may reasonably be off PATH, and telling the user to add it would be advice to fix nothing.
main: argument shift
Read the command, THEN shift — and shift only if there is something there. A bare shift with no positional parameters returns 1, and under set -e that ended the script before a single line was printed: running this with no arguments at all, the documented default, did nothing and said nothing.