-
Notifications
You must be signed in to change notification settings - Fork 0
Add isolated test-environment host installer #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Nickfost
wants to merge
17
commits into
main
Choose a base branch
from
feat/issue-23-tester-host
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
b86cb94
feat: add isolated tester host installer
e2bf688
fix: make tester lifecycle recoverable and scoped
ad5f7c7
test: cover tester cleanup failures
a75e2fa
docs: clarify tester credential boundary
f36fa02
test: support read-only releases as non-root
ee89e26
fix: close remaining tester isolation channels
196b121
fix: make tester lifecycle repairable
ca39b25
fix: serialize tester lifecycle updates
b4736e5
fix: contain tester recovery failures
841406c
fix: quiesce tester timers during activation
d88df91
fix: close tester isolation gaps
bec7c76
fix: route tester lifecycle through stable launcher
7c0e0e3
test: follow dedicated tester lifecycle lock
3c56ef7
fix: make tester uninstall idempotent
c600b8f
Merge remote-tracking branch 'origin/main' into pr-76
a725c98
fix: close final tester lifecycle gaps
e7640e2
fix: restore tester when rollback record fails
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| # Test-environment host | ||
|
|
||
| Status: repository implementation complete; prepared isolated-host acceptance remains required. | ||
|
|
||
| This role runs persistent or expiring deployed application test environments. It is deliberately separate from ephemeral CI workers and production deployers. It cannot register ordinary CI runners or promote production releases. | ||
|
|
||
| ## Boundary | ||
|
|
||
| A tester host accepts only: | ||
|
|
||
| - a reviewed `ci-fleet` source commit for the tester service; | ||
| - root-owned host configuration and environment declarations; | ||
| - application images addressed by an immutable `sha256` digest; | ||
| - environment secrets stored below that environment's fixed host-local secret directory. | ||
|
|
||
| It rejects mutable images, public port binds, host bind mounts, external/unscoped Docker resources, custom volume drivers/options, privileged containers, added capabilities, host or shared namespaces, Docker API access, global container names, and credentials outside the environment secret boundary. Compose environment variables, env files, and configs are forbidden credential channels; use only fixed mode-`0600` Compose secrets. Every service must be read-only, drop all capabilities, and set `no-new-privileges=true`. The validated rendered Compose model is copied into protected runtime state before activation, so partial starts remain tracked and later cleanup does not depend on a mutable or deleted source definition. Test identity, networks, storage, routes, domains, data, and credentials must have no production authority. Host/network isolation is an external acceptance gate, not something this repository-only change can prove. | ||
|
|
||
| ## Prepare host-local configuration | ||
|
|
||
| The installer supports only `/etc/ci-fleet-tester/tester.env`. Create it and all protected directories as root; never commit them: | ||
|
|
||
| ```bash | ||
| sudo install -d -m 0700 \ | ||
| /etc/ci-fleet-tester \ | ||
| /etc/ci-fleet-tester/environments \ | ||
| /etc/ci-fleet-tester/definitions \ | ||
| /etc/ci-fleet-tester/secrets | ||
| sudo install -m 0600 /dev/null /etc/ci-fleet-tester/tester.env | ||
| ``` | ||
|
|
||
| Example non-secret settings: | ||
|
|
||
| ```text | ||
| CI_FLEET_TESTER_DEFAULT_TTL_SECONDS=86400 | ||
| CI_FLEET_TESTER_MAX_ENVIRONMENTS=20 | ||
| CI_FLEET_TESTER_DISK_WARN_PERCENT=80 | ||
| CI_FLEET_TESTER_NETWORK_PROBE_HOST=tester-probe.invalid | ||
| CI_FLEET_TESTER_HTTPS_PROBE_URL=https://tester-probe.invalid/health | ||
| CI_FLEET_TESTER_ISOLATION_ACK=test-only-no-production-authority | ||
| ``` | ||
|
|
||
| Set both probe values to a test-only host whose DNS resolution and HTTPS HEAD response exercise the intended local proxy path without carrying credentials. The acknowledgement is required but is not proof: an authorized operator must still verify that the prepared host has no production identity or network authority. | ||
|
|
||
| For environment `example-preview`, create root-owned mode-`0600` `/etc/ci-fleet-tester/environments/example-preview.env`: | ||
|
|
||
| ```text | ||
| CI_FLEET_TESTER_PROJECT=example-project | ||
| CI_FLEET_TESTER_OWNER=example-owner | ||
| CI_FLEET_TESTER_COMPOSE_FILE=/etc/ci-fleet-tester/definitions/example-preview.yaml | ||
| CI_FLEET_TESTER_EXPIRES_AT=REVIEWED_FUTURE_UNIX_TIME | ||
| CI_FLEET_TESTER_ROUTE_SERVICE=web | ||
| CI_FLEET_TESTER_ROUTE_PORT=18080 | ||
| ``` | ||
|
|
||
| The Compose file is root-owned mode `0644` and may contain no credential value. Each image must use `registry/path@sha256:REVIEWED_64_HEX_DIGEST`. Exactly one route is published, on loopback only, at the declared port. Compose-generated network and volume names must remain below `ci-fleet-test-<environment>_...`; explicit external names are rejected. | ||
|
|
||
| If credentials are required, create `/etc/ci-fleet-tester/secrets/example-preview` as root-owned mode `0700`, put only test-scope regular files there as root-owned mode `0600`, and reference them through Compose `secrets.file`. Symlinks, external secrets, production credentials, environment-variable secret transport, and files outside that exact directory are unsupported. | ||
|
|
||
| ## Fresh install or repair | ||
|
|
||
| Use a clean reviewed checkout at the exact commit: | ||
|
|
||
| ```bash | ||
| ref=$(git rev-parse 'HEAD^{commit}') | ||
| sudo ./scripts/install-tester.sh --install \ | ||
| --config /etc/ci-fleet-tester/tester.env \ | ||
| --ref "$ref" | ||
| ``` | ||
|
|
||
| The command fails before mutation unless it sees Debian 12 or newer, root, the local default Docker context/socket/root, Compose v2, required generic tools, protected paths, and Docker storage below 80%. It stages an immutable source release, validates it, switches the `current` symlink, installs health and expiration timers, and verifies both configuration and active environments. Repeating the same command is idempotent. | ||
|
|
||
| ## Environment lifecycle | ||
|
|
||
| The runtime command is the one interface for create/update, inspect, reset, and removal: | ||
|
|
||
| ```bash | ||
| sudo /opt/ci-fleet-tester/tester-runtime --converge --environment example-preview | ||
| sudo /opt/ci-fleet-tester/tester-runtime --inspect --environment example-preview | ||
| sudo /opt/ci-fleet-tester/tester-runtime --reset --environment example-preview | ||
| sudo /opt/ci-fleet-tester/tester-runtime --remove --environment example-preview | ||
| ``` | ||
|
|
||
| `--converge` validates the full resolved Compose model before `up --wait`. `--reset` removes only that exact Compose project and its volumes, then recreates it from the approved definition and digest. `--remove` uses the same scoped `compose down --volumes`; no global Docker prune is used. State reports only environment/project/owner, loopback route, expiry, source revision, timestamps, and health—not Compose environment values or secret content. | ||
|
|
||
| `ci-fleet-tester-health.timer` checks every five minutes. `ci-fleet-tester-cleanup.timer` checks expiration every fifteen minutes. Expired environments are removed through the same scoped path. Disposable environment data is intentionally removed on reset/expiry. Reviewed definitions and fixture sources belong outside runtime state and need ordinary configuration backups; credentials and disposable volumes are never backed up by this service. | ||
|
|
||
| ## Upgrade, validation, and rollback | ||
|
|
||
| ```bash | ||
| ref=$(git rev-parse 'HEAD^{commit}') | ||
| sudo ./scripts/install-tester.sh --upgrade --config /etc/ci-fleet-tester/tester.env --ref "$ref" | ||
| sudo ./scripts/install-tester.sh --check --config /etc/ci-fleet-tester/tester.env | ||
| sudo ./scripts/install-tester.sh --rollback --config /etc/ci-fleet-tester/tester.env | ||
| ``` | ||
|
|
||
| Upgrade validates the candidate before activation and restores the complete previous release if post-switch checks fail. A successful switch records only the previous complete source revision as last known good. Rollback changes the tester service release; it does not rewrite an application's immutable image digest or reset environment data. | ||
|
|
||
| ## Removal | ||
|
|
||
| Remove every environment explicitly, verify no state remains, then uninstall: | ||
|
|
||
| ```bash | ||
| sudo ./scripts/install-tester.sh --uninstall --config /etc/ci-fleet-tester/tester.env | ||
| ``` | ||
|
|
||
| Uninstall fails while any managed environment exists. It removes only tester units and immutable service releases. Host configuration, definitions, and secrets remain for explicit operator disposition; the script never guesses which credential may be deleted. | ||
|
|
||
| ## External acceptance gate | ||
|
|
||
| Before this draft can merge, an authorized operator must use a prepared isolated Debian Docker host with test-only identity/network/storage/domain boundaries and provide evidence for: fresh install, unchanged second install, immutable application converge, health, scoped reset, expiration cleanup, upgrade, failed-upgrade restoration, rollback, and removal. No production credentials, database, network authority, ordinary CI runner, or live production system may participate. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| [Unit] | ||
| Description=Remove expired ci-fleet test environments | ||
| After=docker.service | ||
| Requires=docker.service | ||
| ConditionPathExists=/etc/ci-fleet-tester/tester.env | ||
|
|
||
| [Service] | ||
| Type=oneshot | ||
| ExecStart=/opt/ci-fleet-tester/tester-runtime --cleanup | ||
| User=root | ||
| Group=root | ||
| UMask=0077 | ||
| NoNewPrivileges=yes | ||
| PrivateTmp=yes | ||
| ProtectHome=yes | ||
| ProtectSystem=strict | ||
| ReadWritePaths=/var/lib/ci-fleet-tester -/run/lock/ci-fleet-tester | ||
| RestrictAddressFamilies=AF_UNIX | ||
| LockPersonality=yes | ||
| MemoryDenyWriteExecute=yes | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| [Unit] | ||
| Description=Expire abandoned ci-fleet test environments | ||
|
|
||
| [Timer] | ||
| OnBootSec=5m | ||
| OnUnitActiveSec=15m | ||
| Persistent=true | ||
| RandomizedDelaySec=1m | ||
| Unit=ci-fleet-tester-cleanup.service | ||
|
|
||
| [Install] | ||
| WantedBy=timers.target |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| [Unit] | ||
| Description=Validate ci-fleet test environments | ||
| After=docker.service | ||
| Requires=docker.service | ||
| ConditionPathExists=/etc/ci-fleet-tester/tester.env | ||
|
|
||
| [Service] | ||
| Type=oneshot | ||
| ExecStart=/opt/ci-fleet-tester/tester-runtime --health | ||
| User=root | ||
| Group=root | ||
| UMask=0077 | ||
| NoNewPrivileges=yes | ||
| PrivateTmp=yes | ||
| ProtectHome=yes | ||
| ProtectSystem=strict | ||
| ReadWritePaths=/var/lib/ci-fleet-tester -/run/lock/ci-fleet-tester | ||
| RestrictAddressFamilies=AF_UNIX | ||
| LockPersonality=yes | ||
| MemoryDenyWriteExecute=yes |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| [Unit] | ||
| Description=Check ci-fleet test environments every five minutes | ||
|
|
||
| [Timer] | ||
| OnBootSec=2m | ||
| OnUnitActiveSec=5m | ||
| Persistent=true | ||
| RandomizedDelaySec=30s | ||
| Unit=ci-fleet-tester-health.service | ||
|
|
||
| [Install] | ||
| WantedBy=timers.target |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| #!/usr/bin/env bash | ||
| set -Eeuo pipefail | ||
| printf '%s\n' "$*" >>"${FAKE_TESTER_DOCKER_LOG:?}" | ||
| [[ -z ${FAKE_TESTER_EVENT_LOG:-} ]] || printf 'docker %s\n' "$*" >>"$FAKE_TESTER_EVENT_LOG" | ||
| if [[ $1 == context && $2 == show ]]; then printf 'default\n'; exit 0; fi | ||
| if [[ $1 == info ]]; then printf '%s\n' "${FAKE_TESTER_DOCKER_ROOT:?}"; exit 0; fi | ||
| if [[ $1 == ps ]]; then [[ ${FAKE_TESTER_PS_FAIL:-0} != 1 ]] || exit 9; printf 'fixture-container-id\n'; exit 0; fi | ||
| if [[ $1 == inspect ]]; then | ||
| if [[ " $* " == *' --size '* ]]; then printf '1024\n'; else printf '%s\n' "${FAKE_TESTER_CONTAINER_STATE:-running healthy}"; fi | ||
| exit 0 | ||
| fi | ||
| if [[ $1 == volume && $2 == ls ]]; then [[ ${FAKE_TESTER_VOLUME_LS_FAIL:-0} != 1 ]] || exit 9; printf 'fixture-volume\n'; exit 0; fi | ||
| if [[ $1 == volume && $2 == inspect ]]; then printf '%s\n' "${FAKE_TESTER_VOLUME_ROOT:?}"; exit 0; fi | ||
| if [[ $1 == compose && $2 == version ]]; then printf 'Docker Compose version v2.fixture\n'; exit 0; fi | ||
| if [[ $1 == compose && $2 == up && ${3:-} == --help ]]; then [[ ${FAKE_TESTER_NO_WAIT_TIMEOUT:-0} != 1 ]] && printf '%s\n' ' --wait-timeout int'; exit 0; fi | ||
| if [[ $1 != compose ]]; then exit 2; fi | ||
| shift | ||
| project= | ||
| while (($#)); do | ||
| case $1 in | ||
| -p) project=$2; shift 2 ;; | ||
| -f) shift 2 ;; | ||
| config|up|down|ps) operation=$1; shift; break ;; | ||
| *) shift ;; | ||
| esac | ||
| done | ||
| case ${operation:-} in | ||
| config) | ||
| digest=$(printf 'a%.0s' {1..64}) | ||
| privileged=false; read_only=true; host_ip=127.0.0.1; image="registry.example/example/app@sha256:$digest"; network_name="${project}_default"; secrets='{}'; service_extra=; top_extra=; volume_extra=; network_extra=; security='no-new-privileges:true' | ||
| case ${FAKE_TESTER_POLICY:-valid} in | ||
| mutable) image=registry.example/example/app:latest ;; | ||
| privileged) privileged=true ;; | ||
| bind) volume='{"type":"bind","source":"/","target":"/host"}' ;; | ||
| broad-port) host_ip=0.0.0.0 ;; | ||
| external-network) network_name=shared ;; | ||
| environment) service_extra=',"environment":{"TOKEN":"example"}' ;; | ||
| configs) service_extra=',"configs":[{"source":"credential"}]'; top_extra=',"configs":{"credential":{"file":"/tmp/example"}}' ;; | ||
| use-api-socket) service_extra=',"use_api_socket":true' ;; | ||
| namespace-share) service_extra=',"network_mode":"service:other"' ;; | ||
| false-nnp) security='no-new-privileges:false' ;; | ||
| custom-volume) volume_extra=',"driver":"local"' ;; | ||
| volumes-from) service_extra=',"volumes_from":["container:other:rw"]' ;; | ||
| custom-network) network_extra=',"driver":"macvlan","driver_opts":{"parent":"eth0"}' ;; | ||
| replicas) service_extra=',"deploy":{"replicas":2}' ;; | ||
| lifecycle-hook) service_extra=',"post_start":[{"command":"true","privileged":true}]' ;; | ||
| gpu) service_extra=',"gpus":"all"' ;; | ||
| deploy-device) service_extra=',"deploy":{"resources":{"reservations":{"devices":[{"capabilities":["gpu"]}]}}}' ;; | ||
| build) service_extra=',"build":{"context":"/"}' ;; | ||
| changed-model) service_extra=',"pull_policy":"always"' ;; | ||
| unconfined) security='no-new-privileges:true","seccomp=unconfined' ;; | ||
| external-links) service_extra=',"external_links":["other-environment-db:db"]' ;; | ||
| userns-host) service_extra=',"userns_mode":"host"' ;; | ||
| cgroup-host) service_extra=',"cgroup":"host"' ;; | ||
| uts-host) service_extra=',"uts":"host"' ;; | ||
| remote-logging) service_extra=',"logging":{"driver":"syslog","options":{"syslog-address":"tcp://example.invalid:514"}}' ;; | ||
| interpolation) [[ -z ${TOKEN:-} ]] || service_extra=$(printf ',"command":["app","--token=%s"]' "$TOKEN") ;; | ||
| valid-secret|outside-secret) secrets=$(printf '{"credential":{"file":"%s"}}' "${FAKE_TESTER_SECRET_FILE:?}") ;; | ||
| esac | ||
| volume=${volume:-'{"type":"volume","source":"data","target":"/data"}'} | ||
| printf '{"services":{"web":{"image":"%s","privileged":%s,"read_only":%s,"cap_drop":["ALL"],"security_opt":["%s"],"volumes":[%s],"ports":[{"host_ip":"%s","published":%s,"target":8080,"protocol":"tcp"}]%s}},"networks":{"default":{"name":"%s"%s}},"volumes":{"data":{"name":"%s_data"%s}},"secrets":%s%s}\n' \ | ||
| "$image" "$privileged" "$read_only" "$security" "$volume" "$host_ip" "${FAKE_TESTER_ROUTE_PORT:-18080}" "$service_extra" "$network_name" "$network_extra" "$project" "$volume_extra" "$secrets" "$top_extra" | ||
| ;; | ||
| up) [[ ${FAKE_TESTER_UP_FAIL:-0} != 1 ]] ;; | ||
| down) [[ ${FAKE_TESTER_DOWN_FAIL:-0} != 1 ]] ;; | ||
| ps) [[ ${FAKE_TESTER_UNHEALTHY:-0} == 1 ]] || printf 'fixture-container-id\n' ;; | ||
| *) exit 2 ;; | ||
| esac |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fresh evidence beyond the accepted launcher recreation fix is this systemd sandbox entry: after reboot, when the volatile directory is absent,
ProtectSystem=strictmakes/run/lockread-only, while the leading-onReadWritePathsmeans the missing exception is ignored. The launcher'smkdirtherefore cannot create the directory when either timer starts, leaving expiration cleanup and health checks broken until an unsandboxed command recreates it; create it withRuntimeDirectory=or grant a writable existing parent.AGENTS.md reference: AGENTS.md:L35-L35
Useful? React with 👍 / 👎.