Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
142 commits
Select commit Hold shift + click to select a range
defd1cf
fix: stop leaking SSH secrets through ValidationError output
AlexMKX Jul 31, 2026
bb16647
docs: design and core plan for run env I/O and the tofu proxy
AlexMKX Jul 31, 2026
53f7832
Merge commit 'defd1cf' into feature/run-env-io
AlexMKX Jul 31, 2026
9c1cc5f
docs: pre-flight fixes to the run-env-io plan
AlexMKX Jul 31, 2026
1fd56e8
feat(exceptions): add MultiNodeEnvUnsupported mapped to exit 1
AlexMKX Jul 31, 2026
4d728f9
fix(envrender): raise MultiNodeEnvUnsupported for the wrong node count
AlexMKX Jul 31, 2026
9495e6c
feat(envrender): predict the injected env-key set from an InputSchema
AlexMKX Jul 31, 2026
9756525
feat(session): cleanup_path reports unremovable paths; add remove_root
AlexMKX Jul 31, 2026
04d9b67
fix(session): report unstatable cleanup paths
AlexMKX Jul 31, 2026
e57a925
feat(session): add silent stop_session primitive returning StopOutcome
AlexMKX Jul 31, 2026
ab385b8
test(session): strengthen silent stop primitive coverage
AlexMKX Jul 31, 2026
b918fe5
refactor(cli): render stop's JSON from the silent stop_session primitive
AlexMKX Jul 31, 2026
b088a81
test(cli): strengthen stop session seam coverage
AlexMKX Jul 31, 2026
52d8680
fix(cli): run teardown writes nothing to stdout and never raises
AlexMKX Jul 31, 2026
93939d8
fix(cli): harden run teardown interruption handling
AlexMKX Jul 31, 2026
e9e8b01
feat(cli_input): build an InputSchema from an environment variable
AlexMKX Jul 31, 2026
59c4c8d
feat(cli): run takes one variadic plus --input-env/--output-var
AlexMKX Jul 31, 2026
825d308
feat(cli): reject an --output-var NAME that collides with an injected…
AlexMKX Jul 31, 2026
0c5af5c
feat(cli): reject connection and daemon flags under --input-env
AlexMKX Jul 31, 2026
6dd62ae
test(cli): strengthen flag-mode conflict controls
AlexMKX Jul 31, 2026
bd76c80
feat(cli): require --output-var for multi-node input, rejected pre-spawn
AlexMKX Jul 31, 2026
d9e5005
feat(cli): inject the full OutputSchema into the child under --output…
AlexMKX Jul 31, 2026
1afa980
test(cli): make the --output-var child-env tests falsifiable
AlexMKX Jul 31, 2026
21949ef
fix(cli): run mints its session path before spawning the daemon
AlexMKX Jul 31, 2026
093faa7
test(cli): exercise the session-dir ownership asymmetry against real …
AlexMKX Jul 31, 2026
9ef90dc
fix(cli): one try/finally owns run's entire post-spawn window
AlexMKX Jul 31, 2026
c47d5ed
fix(cli): keep post-spawn TunstrapError exit codes; restore the black…
AlexMKX Jul 31, 2026
bc4f7f1
test(integration): cover run --input-env/--output-var end to end
AlexMKX Jul 31, 2026
8d134b3
test(integration): pin run's stdout to the child's bytes exactly
AlexMKX Aug 1, 2026
7fc9b59
docs(readme): document run --input-env, --output-var and the -- rule
AlexMKX Aug 1, 2026
e57ebd1
chore: stabilize ruff CI gates
AlexMKX Aug 1, 2026
52621f4
fix(session): reap the daemon so the grace poll ends when it exits
AlexMKX Aug 1, 2026
333de9e
fix(daemon): stop the worker when the parent-side handshake fails
AlexMKX Aug 1, 2026
76f7c05
fix(cli): normalise signalled child exit codes; clear ledger minors
AlexMKX Aug 1, 2026
3cc5dc5
style(tests): sort imports and drop the I001 suppression
AlexMKX Aug 1, 2026
b5aee2f
test(cli_input): isolate include_input=False from the _scrub layer
AlexMKX Aug 1, 2026
f6339e7
docs: implementation plan for the e2e tier
AlexMKX Aug 1, 2026
d596b77
test: declare the e2e marker and keep it out of the default selection
AlexMKX Aug 1, 2026
aeee1d7
test(e2e): compose rig with the mandatory sshd forwarding drop-in
AlexMKX Aug 1, 2026
b0ae969
test(e2e): own keypair, preflight, and the self-containment guards
AlexMKX Aug 1, 2026
f36d9d0
test(e2e): kind cluster fixture and the in-node kubectl oracle
AlexMKX Aug 1, 2026
832fe95
test(e2e): reject relative cross-suite imports
AlexMKX Aug 1, 2026
0f38f66
test(e2e): cover cluster creation in teardown, harden _kube, earn the…
AlexMKX Aug 1, 2026
073e532
test(e2e): compose lifecycle with authenticated SSH readiness
AlexMKX Aug 1, 2026
eb74008
test(e2e): prove the tunnel carries real kube API traffic
AlexMKX Aug 1, 2026
53afb90
test(e2e): bound each SSH readiness attempt and arm compose teardown …
AlexMKX Aug 1, 2026
b3b2750
test(e2e): the tofu module and probe chart under test
AlexMKX Aug 1, 2026
f80a06a
test(e2e): the tofu shim, its negative control, and the drift guard
AlexMKX Aug 1, 2026
06525ab
test(e2e): tofu fixtures, recorder harness, and the inert-branch plan
AlexMKX Aug 1, 2026
537166a
test(e2e): apply, chain integrity and destroy against a real cluster
AlexMKX Aug 1, 2026
5397924
test(e2e): negative control - apply must fail without TF_VAR_tunstrap
AlexMKX Aug 1, 2026
bb45a34
test(e2e): a real provider failure still surfaces as non-zero
AlexMKX Aug 1, 2026
d83f6b7
test(e2e): prove init/-version pass-through with a poisoned payload
AlexMKX Aug 1, 2026
ad87f13
test(e2e): the child's exit code 42 reaches the caller verbatim
AlexMKX Aug 1, 2026
d85baa5
test(e2e): harden write_fake_tofu to emit adversarial stdout bytes ve…
AlexMKX Aug 1, 2026
a983e67
test(e2e): pin the shim's stdout to the child's bytes exactly
AlexMKX Aug 1, 2026
4d72ca0
ci: add the e2e job, kept out of the coverage combine
AlexMKX Aug 1, 2026
bb0de74
docs(tests): document the e2e suite, its fixtures and prerequisites
AlexMKX Aug 1, 2026
f48bf6f
ci(e2e): tighten the floor to 24 and add a job timeout
AlexMKX Aug 1, 2026
aa01cab
docs(tests): drop the host kubectl prerequisite for e2e
AlexMKX Aug 1, 2026
650455d
docs(e2e): restore host kubectl prereq; rescope conftest comment
AlexMKX Aug 1, 2026
ce66bbf
docs(tests): correct cache timing claim, REQUIRE_ALL gap, and two com…
AlexMKX Aug 1, 2026
81677e6
docs(recipe): add Terragrunt/OpenTofu CLI-proxy recipe; fix stale tok…
AlexMKX Aug 1, 2026
083b36b
fix(cli): stop shipping kube credentials through --output-var
AlexMKX Aug 1, 2026
23d81ad
fix(cli): scrub the --input-env variable from the child environment
AlexMKX Aug 1, 2026
b31573d
docs(run-env-io): document the fetch_files content_b64 projection asy…
AlexMKX Aug 1, 2026
903293f
test(e2e): pin the terragrunt recipe's HCL against real terragrunt
AlexMKX Aug 2, 2026
22473d9
test(e2e): drive real terragrunt apply/destroy through the shim
AlexMKX Aug 2, 2026
03456d5
test(e2e)+docs(recipe): pin shim/module snippets, decode both ternary…
AlexMKX Aug 2, 2026
a410db3
ci: raise e2e floor to 31; annotate tool pins for Renovate
AlexMKX Aug 2, 2026
543974e
test(e2e): prove tunnelled-output stdout purity via tunstrap run
AlexMKX Aug 2, 2026
a229923
test(unit): guard kubectl/node-image version coupling; review minors
AlexMKX Aug 2, 2026
2425fb6
feat(tofu-proxy): ship tunstrap_tofu console entry point in-package
AlexMKX Aug 2, 2026
b891d0d
fix(tofu-proxy): bypass deny-list, not cluster allow-list; review fixes
AlexMKX Aug 2, 2026
dd62372
perf(init): resolve __version__ lazily; drop ~42 ms from pass-through
AlexMKX Aug 2, 2026
59944ca
docs(tofu-proxy): correct cost prose after lazy __version__; harden v…
AlexMKX Aug 2, 2026
f1dc328
feat(recipe,e2e): migrate the tier and recipe to tunstrap_tofu
AlexMKX Aug 2, 2026
d1fa96e
fix(recipe): restore run_cmd with its load-bearing marker; correct fa…
AlexMKX Aug 2, 2026
6e69163
fix(recipe,e2e): join root+unit via include; catch the silent non-inh…
AlexMKX Aug 3, 2026
8978793
test(unit): make version + flag-mode tests CI-environment-independent
AlexMKX Aug 3, 2026
1e54a7b
docs: compress plan scaffolding into a decision history
AlexMKX Aug 3, 2026
f35a6e5
docs(spec): record the ruff pin breakage as resolved, not open
AlexMKX Aug 3, 2026
312cb81
fix: report tofu launch failures cleanly
AlexMKX Aug 4, 2026
eb5d454
fix: honor payload shutdown grace
AlexMKX Aug 4, 2026
4a6d2b4
fix: preserve failed teardown session data
AlexMKX Aug 4, 2026
2280772
docs(test): drop private org identifiers from a public-repo docstring
AlexMKX Aug 4, 2026
321d25a
fix: scrub input from daemon environment
AlexMKX Aug 4, 2026
b5d7ffa
fix(cli): replace bare assert with TunstrapError in run_via_env_input
AlexMKX Aug 4, 2026
9b6f904
test(ci): make the kubectl coupling guard reject a second dl.k8s.io URL
AlexMKX Aug 4, 2026
12ec034
docs(recipe): fix the ssh_private_key example - it was a public key line
AlexMKX Aug 4, 2026
908994c
fix(e2e): write_tofu_recorder must honor skip_or_fail's guard, not by…
AlexMKX Aug 4, 2026
7374222
fix(e2e): harden write_tofu_recorder diagnostics to 0700/0600
AlexMKX Aug 4, 2026
b59dbe6
docs(tests): document the e2e tier's parallel-safety deviation
AlexMKX Aug 4, 2026
afd4128
fix(cli): a raising teardown preserves session data too
AlexMKX Aug 4, 2026
2bc2a7d
fix(daemon): scrub the variable --input-env actually names, not a lit…
AlexMKX Aug 4, 2026
f31f414
fix(cli): emit a recovery command that tunstrap can actually parse
AlexMKX Aug 4, 2026
d576a17
fix(session): tell a missing identity from an unreadable one
AlexMKX Aug 4, 2026
0e764d7
docs: match the teardown contract and complete the --input-env matrix
AlexMKX Aug 4, 2026
c470780
test(e2e): assert the recorder's 0700/0600 diagnostic modes
AlexMKX Aug 4, 2026
85bf52b
fix(cli): stop must not delete what it failed to stop
AlexMKX Aug 4, 2026
288ecb5
fix(cli): stop's identity-read failures must report that they preserved
AlexMKX Aug 4, 2026
e066de1
docs(pyproject): stop duplicating the fast-path cost measurement
AlexMKX Aug 5, 2026
510835e
fix(cli): remove run_via_env_input's dead grace_seconds parameter
AlexMKX Aug 5, 2026
4fe9cb6
refactor(cli,fetcher): split the two CC-18 functions into orchestration
AlexMKX Aug 5, 2026
80f07ba
chore: drop three stale suppressions, correct a fourth to what fires
AlexMKX Aug 5, 2026
1136409
chore: actually delete vulture_whitelist.py (completes 80f07ba)
AlexMKX Aug 5, 2026
a799aec
refactor(kube): split parse_kubeconfig into orchestrator + section pa…
AlexMKX Aug 5, 2026
5eb49bf
test(kube): pin the two parse_kubeconfig properties that look redundant
AlexMKX Aug 5, 2026
e5ed15d
chore(ssh): drop the last useless lint suppression
AlexMKX Aug 5, 2026
3eeea11
docs(issue15): spec, decision history, and implementation plan for ku…
AlexMKX Aug 8, 2026
22c610e
feat(kube): rename current-context identity to tunstrap-<node>-<targe…
AlexMKX Aug 8, 2026
3974ca3
feat(schemas): reject tunstrap-<node>-<target> naming collisions (#15)
AlexMKX Aug 8, 2026
165fcae
test(kube): regression test for the k3s upstream-name collision trap …
AlexMKX Aug 8, 2026
586858b
feat(envrender): multi-node kube channel + conditional KUBE_CONFIG_PA…
AlexMKX Aug 8, 2026
86c2e25
test(envrender): cover unmaterialized kube channel targets (#15)
AlexMKX Aug 8, 2026
22079bd
feat(envrender): unified node-qualified output contract, shape only (…
AlexMKX Aug 8, 2026
c16b03a
feat: unified output materialization; remove TUNSTRAP_* scalars, Mult…
AlexMKX Aug 8, 2026
3d39fe2
test(integration): retarget env-shape assertions for the unified outp…
AlexMKX Aug 8, 2026
4c9e2ff
fix(cli): narrow OSError handling so materialization failures report …
AlexMKX Aug 8, 2026
3a8ee12
test(e2e): retarget to the unified output nodes.*.kube.*.path shape (…
AlexMKX Aug 8, 2026
324c232
docs(recipe): kubeconfig-as-identity delivery + unified output + stab…
AlexMKX Aug 8, 2026
5c724dc
test: bind assert messages to locals to resolve black/ruff-format wra…
AlexMKX Aug 8, 2026
fc2ea3b
fix: drop stale RED/GREEN docstring framing, dedupe survivor scalars,…
AlexMKX Aug 8, 2026
a3c2e43
fix(cli): narrow suppress_kubeconfig to keep Mode A working through t…
AlexMKX Aug 8, 2026
8936013
feat(tofu_proxy): bypass validate/fmt -- provably cluster-free, same …
AlexMKX Aug 8, 2026
75e3d68
fix: address review minors on the Mode A fix (#14)
AlexMKX Aug 8, 2026
d72850b
fix(session): refuse a non-positive pid on both syscall paths (#17)
AlexMKX Aug 9, 2026
29d1707
fix(session): stop a caller-supplied session dir from redirecting our…
AlexMKX Aug 9, 2026
9b0a87c
fix(session): check every raw-fd write, and never orphan the temp (#21)
AlexMKX Aug 9, 2026
f036873
fix(manager): give fetched files and kubeconfigs separate on-disk slo…
AlexMKX Aug 9, 2026
2a88d02
fix(cli): stop start's JSON from reprinting secrets it already wrote …
AlexMKX Aug 10, 2026
6e8a80d
fix(kube): refuse a fetched kubeconfig that already occupies our name…
AlexMKX Aug 10, 2026
2d285b9
refactor: replace the last production asserts and let ruff enforce it…
AlexMKX Aug 10, 2026
23ed920
fix(envrender): reserve the kube env names run always scrubs (#23)
AlexMKX Aug 10, 2026
08babf6
fix(cli): stop start from losing the handle to a daemon it just spawn…
AlexMKX Aug 10, 2026
4556a4a
fix(daemon): bound the startup handshake and every SFTP fetch (#24)
AlexMKX Aug 10, 2026
1b7de45
chore: housekeeping cluster -- stop exit code, dead branch, docstring…
AlexMKX Aug 10, 2026
b14256f
docs: correct README's env and output contracts to what the code emit…
AlexMKX Aug 10, 2026
a3a8b66
docs: stop leaking a developer path, and make every citation resolve …
AlexMKX Aug 10, 2026
36e3e04
docs: cite symbols, not line numbers, and pin that they resolve (#28c)
AlexMKX Aug 10, 2026
a63dd8a
refactor: collapse structure that never earned its complexity
AlexMKX Aug 10, 2026
7abc12c
fix(session): mint a supplied session dir's ancestors 0700, not at um…
AlexMKX Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- run: ruff format --check .
- run: ruff check .
- run: pylint tunstrap/
- run: vulture tunstrap/ vulture_whitelist.py
- run: vulture tunstrap/
- run: mypy --strict tunstrap
- name: combined unit + integration coverage gate
run: |
Expand Down
122 changes: 121 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
run: pylint tunstrap/
- name: vulture
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
run: vulture tunstrap/ vulture_whitelist.py
run: vulture tunstrap/
- name: mypy --strict
run: mypy --strict tunstrap
- name: pytest (unit)
Expand Down Expand Up @@ -84,6 +84,126 @@ jobs:
if-no-files-found: error
retention-days: 1

e2e:
# Real kind cluster + real OpenTofu providers through a real tunnel.
#
# A separate job, not part of `integration`: the integration marker's
# contract is "docker compose alone", while this needs kind, tofu, kubectl,
# a 1.45 GB node image and an external Docker network. Folding it in would
# break that contract and make every integration run pay ~90s of cluster
# setup.
#
# It produces NO coverage artifact and is deliberately absent from the
# `coverage` job's combine: that gate downloads exactly two artifacts and
# enforces --fail-under=80, and a third, slowest, most environment-sensitive
# input would let a cluster flake take the coverage gate down with it.
#
# If this proves flaky, demote it with `continue-on-error: true` here rather
# than by weakening any assertion in tests/e2e/.
runs-on: ubuntu-latest
# Bounds a hung `kind create`, a stuck image pull, or a wedged provider
# download. Realistic runtime is a few minutes (cluster create + provider
# downloads dominate; per-init registry version-resolution is the slowest
# step); this has carried a stale local number twice, so it is intentionally
# qualitative. 20 min is a comfortable multiple of that peak and well under
# the 360m Actions default a true hang would otherwise ride.
timeout-minutes: 20
needs: unit
env:
# Turns every "tool missing" skip in tests/e2e/rig.py::skip_or_fail into a
# failure. Without it a runner that lost `kind` between the install step
# and the test step would report a green job with most of the tier
# skipped - a cluster tier that silently stops running is worse than none.
TUNSTRAP_E2E_REQUIRE_ALL: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: python -m pip install --upgrade pip
- run: pip install -e ".[dev]"
- name: install kind
run: |
curl -fsSL -o ./kind https://kind.sigs.k8s.io/dl/v0.30.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
kind version
- name: install kubectl
# Matched to kindest/node:v1.34.0, which tests/e2e/rig.py pins.
run: |
curl -fsSL -o ./kubectl https://dl.k8s.io/release/v1.34.0/bin/linux/amd64/kubectl
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin/kubectl
kubectl version --client
- name: install terragrunt
# tests/e2e/test_recipe_terragrunt.py drives real `terragrunt hcl validate`
# and `terragrunt render` against the HCL pinned in docs/recipe_terragrunt.md.
# Pinned to 1.1.1 - the version the recipe's "Measured Terragrunt facts"
# section is measured against; the recipe's CLI vocabulary is
# version-specific (hcl validate / render / run --all).
run: |
curl -fsSL -o ./terragrunt https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_amd64
chmod +x ./terragrunt
sudo mv ./terragrunt /usr/local/bin/terragrunt
terragrunt --version
- uses: opentofu/setup-opentofu@v1
with:
tofu_version: 1.12.5
# MANDATORY. The action's default wrapper replaces `tofu` on PATH with
# a script that adds GitHub annotations to real tofu's output, and this
# tier asserts on that output. The real-tofu path lives in
# test_tofu_providers.py (init/plan/apply/destroy call `tofu` directly,
# or a recorder that execs it); test_shim.py is unaffected because it
# puts a fake `tofu` first on PATH, which shadows any wrapper.
tofu_wrapper: false
- run: docker compose version
- name: pytest (e2e)
run: pytest tests/e2e -m e2e -v --junitxml=e2e-results.xml
- name: require the whole tier to have run
# Second, independent guard. TUNSTRAP_E2E_REQUIRE_ALL covers a missing
# tool; this covers everything else that can quietly shrink the tier -
# a stray @pytest.mark.skip, a collection error swallowed by -k, a
# deleted module. stdlib only, exact numbers, no log scraping.
if: always()
run: |
python - <<'PY'
import xml.etree.ElementTree as ET

# pytest writes <testsuites><testsuite .../></testsuites>; accept both
# shapes so a pytest change cannot make this silently read zeros.
root = ET.parse("e2e-results.xml").getroot()
suite = root if root.tag == "testsuite" else root.find("testsuite")
assert suite is not None, "no <testsuite> element in the JUnit report"
counts = {k: int(suite.get(k, 0)) for k in ("tests", "skipped", "failures", "errors")}
print(counts)
assert counts["skipped"] == 0, "the e2e tier must never skip in CI"
assert counts["failures"] == 0 and counts["errors"] == 0
# Floor, not equality: adding tests must not require a CI edit, but
# losing them must fail. Kept as a hand-set literal deliberately: a
# number derived by collecting the suite would be computed from the
# same files pytest runs, so a deleted module shrinks the collected
# count and the JUnit `tests` count in lockstep and the guard would
# stay GREEN for exactly the deletion it exists to catch (proven
# tautological). Only an independent literal anchors against deletion;
# at 31 == the current tier it catches the loss of any one test. Bump
# to the new total whenever the tier grows. (Was 32; lowered to 31 by
# the tunstrap_tofu migration: retired 4 shell-shim-text/drift tests
# that the in-package proxy makes obsolete, added 3 proxy/install/gap-
# fix replacements, net -1. test_terragrunt_apply's pollution sub-check
# retired too - the proxy runs run in-process so the tunstrap-wrapper
# negative control no longer reaches the stream; purity is now proven
# by the byte-equality test. Raised back to 33 by the run_cmd follow-up:
# +1 test pinning the load-bearing `--terragrunt-quiet` run_cmd marker
# (proves the parse breaks with the marker removed) and +1 pinning the
# labelled shell-shim-alt alternative fence (sh -n + bypass smoke).)
# Raised to 34 by the inheritance follow-up: +1 test proving a unit that
# forgets `include "root"` is caught (apply fails at 127.0.0.1:0) AND
# distinguished from a forgotten-commands entry (TUNSTRAP_INPUT present
# vs absent) - after the rig stopped concatenating root+unit into one file.
assert counts["tests"] >= 34, f"expected at least 34 e2e tests, ran {counts['tests']}"
print("e2e tier ran in full")
PY

coverage:
runs-on: ubuntu-latest
needs: integration
Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,12 @@ Thumbs.db
# Integration test SSH keypair (generated per session)
tests/integration/_keys/

# E2E rig: own SSH keypair and the extracted in-node kubeconfig.
# Both are generated per session by tests/e2e/conftest.py. The e2e tier
# deliberately does not share tests/integration/_keys/, which is generated by
# a fixture pytest never loads for tests/e2e and would be absent in a clean
# checkout.
tests/e2e/_keys/
tests/e2e/_kube/

/docs/artifacts/
Loading
Loading