From 7fdf8baccaa57ade3291faf70cada357352066db Mon Sep 17 00:00:00 2001 From: Trey Date: Thu, 1 Oct 2026 15:30:17 +0300 Subject: [PATCH 1/2] feat(mecak8s-kind): add one-shot kind-up and kind-down tasks Compose the existing fixture targets into a single bring-up and teardown: - kind-up: kind-keycloak-setup, build, kind-hosts-add, kind-keycloak-demo, run sequentially. It recreates mecatl-dev, so it carries a Task prompt guard; without a terminal Task cancels it unless --yes is passed. - kind-down: mecatui logout (best effort), kind-hosts-remove, kind-destroy. kind-hosts-add and kind-hosts-remove now read the world-readable /etc/hosts unprivileged first and reach sudo only when an entry must change, so a converged run never prompts and removal no longer overwrites /etc/hosts.bak when there is nothing to remove. TestMecak8sKindFixture_OneShotLifecycle pins the prompt guard, the step order of both tasks, and the check-before-sudo order of the hosts tasks. Co-Authored-By: Claude Opus 5.5 --- .../mecak8s-kind-manual-verify/SKILL.md | 8 +-- deploy/mecak8s-kind/README.md | 24 ++++++++- deploy/mecak8s-kind/Taskfile.yml | 45 ++++++++++++++++- deploy/mecak8s-kind/fixture_test.go | 50 +++++++++++++++++++ 4 files changed, 122 insertions(+), 5 deletions(-) diff --git a/.claude/skills/mecak8s-kind-manual-verify/SKILL.md b/.claude/skills/mecak8s-kind-manual-verify/SKILL.md index 55b01e86e0..ac10921a9f 100644 --- a/.claude/skills/mecak8s-kind-manual-verify/SKILL.md +++ b/.claude/skills/mecak8s-kind-manual-verify/SKILL.md @@ -20,8 +20,9 @@ provisioning. - Working directory is the mecatl repo (or a worktree of it). - `ko`, `kubectl`, `kind`, and either `docker` or `podman` installed. - The `mecatl-dev` Kind cluster already exists (`kind get clusters` shows it). - If it doesn't, run `task mecak8s:kind-setup` first — that's provisioning, - not this skill's job. + If it doesn't, run `task mecak8s:kind-setup` (base) or + `task mecak8s:kind-up` (one-shot Keycloak bring-up) first — that's + provisioning, not this skill's job. ## Step 1 — Refresh the mecak8s image with current source @@ -93,7 +94,8 @@ the TUI/client code changed. This skill never tears the cluster down. To destroy the whole fixture: ```sh -task mecak8s:kind-destroy +task mecak8s:kind-down # logout, remove /etc/hosts aliases, destroy +task mecak8s:kind-destroy # cluster and local state only ``` ## Reference diff --git a/deploy/mecak8s-kind/README.md b/deploy/mecak8s-kind/README.md index 1f18b4a407..8b30759779 100644 --- a/deploy/mecak8s-kind/README.md +++ b/deploy/mecak8s-kind/README.md @@ -80,13 +80,35 @@ task mecak8s:kind-hosts-remove `kind-hosts-add` manages two entries in `/etc/hosts`: `127.0.0.1 keycloak.mecatl.svc.cluster.local` and `127.0.0.1 mecak8s-mecak8s.mecatl.svc.cluster.local`; `kind-hosts-remove` removes -only those exact entries and leaves an `/etc/hosts.bak` backup. The Keycloak +only those exact entries and leaves an `/etc/hosts.bak` backup. Both tasks read +`/etc/hosts` without privileges first and call `sudo` only when an entry must +change, so a run where nothing needs to change never asks for a password. The Keycloak NodePort is mapped by Kind only to `127.0.0.1:8443` on the host. Keycloak's alias preserves its configured issuer and certificate hostname while making the local browser leg reachable. The mecak8s alias exists for a different, less obvious reason -- see the footgun note below; it is not merely a second convenience name. +### One-shot bring-up and teardown + +To run the whole journey in one command, use the two composed tasks: + +```sh +task mecak8s:kind-up # asks for confirmation; sudo only if an alias is missing +task mecak8s:kind-down # sudo only if an alias is present +``` + +`kind-up` runs `kind-keycloak-setup`, `task build`, `kind-hosts-add`, and +`kind-keycloak-demo` in order. It is **destructive** because it deletes and +recreates `mecatl-dev`, so it asks for confirmation first. Without a terminal, +Task cancels it unless you pass `--yes`. `kind-down` runs `mecatui logout` +against the fixture target, then `kind-hosts-remove` and `kind-destroy`. It +ignores a missing binary or an absent login, and you can run it again safely. + +The aliases are the same for every cluster. After you run +`task mecak8s:kind-hosts-add` once in a terminal, `task --yes mecak8s:kind-up` +runs without any prompt, for example from a script. + ### Direct remote-client quickstart After `kind-keycloak-setup` and the explicit `kind-hosts-add` step above, run: diff --git a/deploy/mecak8s-kind/Taskfile.yml b/deploy/mecak8s-kind/Taskfile.yml index d15a28c3c8..ea39ca8667 100644 --- a/deploy/mecak8s-kind/Taskfile.yml +++ b/deploy/mecak8s-kind/Taskfile.yml @@ -37,6 +37,34 @@ tasks: - task: kind-setup - task: kind-keycloak-apply + # kind-up and kind-down are one-shot compositions of the targets below. They + # run sequentially through cmds, never deps, which Task runs in parallel. + kind-up: + desc: >- + One-shot, DESTRUCTIVE bring-up: recreate mecatl-dev with the Keycloak + layer, build bin/mecatui, add any missing /etc/hosts aliases (sudo only then), + export the fixture CA, and print the login/connect commands. Pass --yes + to skip the confirmation. + prompt: This deletes and recreates the mecatl-dev Kind cluster and its local state. Continue? + cmds: + - task: kind-keycloak-setup + - task: :build + - task: kind-hosts-add + - task: kind-keycloak-demo + + kind-down: + desc: >- + One-shot teardown: log mecatui out of the fixture target, remove any + present /etc/hosts aliases (sudo only then), and delete the cluster and its + local state. Safe to re-run. + cmds: + # Logout precedes cleanup so no stored token outlives the realm that + # issued it; a missing binary or absent login is not a teardown failure. + - cmd: test -x ./bin/mecatui && ./bin/mecatui logout mecak8s-mecak8s.mecatl.svc.cluster.local:18080 + ignore_error: true + - task: kind-hosts-remove + - task: kind-destroy + # kind-fixture-ca-refresh waits for the three direct loopback mappings and # (re)writes .scratch/kind/mecatl-dev/fixture-ca.crt from the cluster's # CURRENT fixture-ca Secret. cert-manager mints a fresh self-signed CA on @@ -161,7 +189,7 @@ tasks: printf '%s\n' '127.0.0.1 mecak8s-mecak8s.mecatl.svc.cluster.local' kind-hosts-add: - desc: Add the temporary Keycloak issuer and mecak8s target aliases to /etc/hosts (loopback only) + desc: Add the temporary Keycloak issuer and mecak8s target aliases to /etc/hosts (loopback only; sudo only for a missing entry) cmds: - cmd: | set -eu @@ -174,9 +202,17 @@ tasks: # that check, so the mecak8s alias below -- resolving to the SAME # 127.0.0.1 the Kind mapping already exposes -- is the fix, not a # cosmetic alternative to `localhost`. + # + # /etc/hosts is world-readable, so presence is checked unprivileged + # first: sudo (and its password prompt) is reached only for a missing + # entry, which lets kind-up run non-interactively once the aliases + # exist. The privileged re-check keeps the append idempotent. for entry in \ '127.0.0.1 keycloak.mecatl.svc.cluster.local' \ '127.0.0.1 mecak8s-mecak8s.mecatl.svc.cluster.local'; do + if grep -Fqx "$entry" /etc/hosts; then + continue + fi sudo sh -c 'grep -Fqx "$1" /etc/hosts || printf "%s\\n" "$1" >> /etc/hosts' sh "$entry" done echo 'Added (or already present):' @@ -187,6 +223,13 @@ tasks: cmds: - cmd: | set -eu + # Checked unprivileged first, as in kind-hosts-add: with neither entry + # present there is nothing to remove, so sudo is never reached and + # /etc/hosts.bak is not overwritten. + if ! grep -Fqx -e '127.0.0.1 keycloak.mecatl.svc.cluster.local' -e '127.0.0.1 mecak8s-mecak8s.mecatl.svc.cluster.local' /etc/hosts; then + echo 'No temporary entries present; nothing to remove.' + exit 0 + fi sudo sed -i.bak \ -e '\|^127\.0\.0\.1 keycloak\.mecatl\.svc\.cluster\.local$|d' \ -e '\|^127\.0\.0\.1 mecak8s-mecak8s\.mecatl\.svc\.cluster\.local$|d' \ diff --git a/deploy/mecak8s-kind/fixture_test.go b/deploy/mecak8s-kind/fixture_test.go index e1bc538785..0d41917ac5 100644 --- a/deploy/mecak8s-kind/fixture_test.go +++ b/deploy/mecak8s-kind/fixture_test.go @@ -452,6 +452,56 @@ func TestMecak8sKindFixture_Scenario3_KeycloakDemoQuickstart(t *testing.T) { } } +// TestMecak8sKindFixture_OneShotLifecycle pins the one-shot compositions: the +// destructive bring-up is confirmation-guarded and orders hosts aliases before +// the readiness/CA step, the hosts steps reach sudo only when there is work, +// and teardown logs out before deleting the cluster. +func TestMecak8sKindFixture_OneShotLifecycle(t *testing.T) { + upBlock := fixtureTaskBlock(t, "kind-up") + if !strings.Contains(upBlock, "prompt:") { + t.Fatal("kind-up recreates the cluster and must carry a prompt guard") + } + if strings.Contains(upBlock, "deps:") { + t.Fatal("kind-up must run its steps sequentially through cmds, not parallel deps") + } + assertOrdered(t, upBlock, "task: kind-keycloak-setup", "task: :build", "task: kind-hosts-add", "task: kind-keycloak-demo") + + // /etc/hosts is world-readable: both hosts tasks check it unprivileged + // before reaching sudo, so an already-converged run never prompts. + assertOrdered(t, fixtureTaskBlock(t, "kind-hosts-add"), `if grep -Fqx "$entry" /etc/hosts`, "continue", "sudo sh -c") + assertOrdered(t, fixtureTaskBlock(t, "kind-hosts-remove"), "if ! grep -Fqx", "exit 0", "sudo sed -i.bak") + + downBlock := fixtureTaskBlock(t, "kind-down") + assertOrdered(t, downBlock, "mecatui logout mecak8s-mecak8s.mecatl.svc.cluster.local:18080", "task: kind-hosts-remove", "task: kind-destroy") +} + +// fixtureTaskBlock returns only the named task's own block, after verifying +// that every task it transitively references exists. +func fixtureTaskBlock(t *testing.T, name string) string { + t.Helper() + closure := fixtureTaskClosure(t, name) + headers := fixtureTaskBlockRe.FindAllStringIndex(closure, 2) + if len(headers) < 2 { + return closure + } + return closure[:headers[1][0]] +} + +func assertOrdered(t *testing.T, text string, steps ...string) { + t.Helper() + prev := -1 + for _, step := range steps { + idx := strings.Index(text, step) + if idx < 0 { + t.Fatalf("missing step %q", step) + } + if idx < prev { + t.Fatalf("step %q is out of order", step) + } + prev = idx + } +} + // TestMecak8sKindFixture_Scenario3_KeycloakOIDCOverlay pins the disposable // private-HTTPS OIDC shape. The CA is narrowly mounted for the validator and // no process-wide or deprecated insecure escape hatch is admitted. From 430c51f8708776835e1d2a3e2370f3b73c0d1ccb Mon Sep 17 00:00:00 2001 From: Trey Date: Thu, 1 Oct 2026 15:46:35 +0300 Subject: [PATCH 2/2] chore(gitignore): ignore the mecak8s-kind fixture kubeconfig kind-setup writes deploy/mecak8s-kind/kconfig.yaml (cluster-admin client credentials for the disposable mecatl-dev cluster). Unlike the sibling mecak8s-vmcp fixture kubeconfig it was not ignored, so a stray `git add` could commit it. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8f3a45a4df..4b1f51cd3d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,8 @@ coverage.* # Retired generated documentation index; do not re-add without an explicit decision. /llms.txt -# Disposable ToolHive Kind fixture kubeconfig +# Disposable Kind fixture kubeconfigs +/deploy/mecak8s-kind/kconfig.yaml /deploy/mecak8s-vmcp/kconfig.yaml # Task cache (root and per-Taskfile-include, e.g. website/.task/)