-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Docker daemon network policy apply engine stage #83
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
Merged
Merged
Changes from 3 commits
Commits
Show all changes
54 commits
Select commit
Hold shift + click to select a range
ed8ad09
feat: add Docker daemon config renderer for network policy
Nickfost 48509ec
feat: add Docker daemon network policy apply engine stage
Nickfost eb131ce
fix: make Docker network policy apply transaction safe
Nickfost c8848aa
fix: require safe drain and report rollback failures
Nickfost 5ca6e04
fix: harden Docker network policy file transaction
Nickfost 5a12d4a
fix: close Docker policy verification gaps
Nickfost be86098
fix: restore prior Docker policy on desired-state removal
Nickfost 17bedc2
fix: harden Docker policy command boundaries
Nickfost ebfbc27
fix: preserve Docker policy file durability
Nickfost 42775b6
fix: verify Docker policy runtime transitions
Nickfost eed2bec
fix: preserve Docker settings on policy removal
Nickfost c21a620
docs: document Docker policy rollout gates
Nickfost 11ca1ce
fix: preserve warning health fixtures in CI
Nickfost 88ab763
fix: preserve health operational controls
Nickfost e20c894
fix: validate network policy inputs before drain
Nickfost e789a8a
fix: verify capacity after network policy removal
Nickfost a0fb1a5
fix: enforce trusted network policy paths
Nickfost 4b81478
fix: retain durable network policy recovery state
Nickfost e5ffbdc
fix: resume controller after failed policy drain
Nickfost 12707b1
fix: persist interrupted policy removal state
Nickfost 89cf008
fix: separate policy checkpoint and daemon paths
Nickfost e76fcae
fix: validate rendered network policy capacity
Nickfost 82d02a0
fix: abort policy apply on daemon conflicts
Nickfost 53b829d
fix: trust rendered policy environment paths
Nickfost 2e1e220
fix: persist durable policy rollback recovery
Nickfost 035c16a
fix: abort policy removal on daemon conflicts
Nickfost ebdb964
fix: open installer lock through trusted path
Nickfost 1615e2d
fix: recover safe interrupted policy removal
Nickfost 452aede
fix: reject explicit empty network policy
Nickfost 91a25b3
fix: validate network policy transaction paths
Nickfost fe04f50
fix: harden network policy recovery
Nickfost c131467
fix: serialize network policy recovery
Nickfost f0eb7e5
fix: redact rendered network policy failures
Nickfost f08cbc0
fix: close network policy recovery gaps
Nickfost 6aeae2e
fix: close network policy recovery lock gaps
Nickfost 4ac9ec7
fix: harden network policy retry rollback
Nickfost ba6cf53
test: make daemon metadata regression portable
Nickfost 94268a2
fix: harden network policy recovery durability
Nickfost 37212a2
fix: harden network policy recovery ordering
Nickfost ed928a3
fix: harden network policy reapply consistency
Nickfost 18070a2
fix: harden network policy rollback recovery
Nickfost b020855
fix: preserve network policy removal provenance
Nickfost 9d20100
fix: harden network policy reconciliation
Nickfost 95d8bcc
fix: bound network policy transaction inputs
Nickfost abf1a84
fix: complete network policy recovery validation
Nickfost 148f0d2
fix: close network policy validation gaps
Nickfost 777af29
fix: complete network policy validation closure
Nickfost 78de795
fix: reject daemon paths within checkpoints
Nickfost 3aa7fa7
fix: close network policy recovery gaps
Nickfost 240f70b
fix: redrain before interrupted recovery fallback
Nickfost 5db6b72
fix: parse rendered health environment safely
Nickfost 05ca901
fix: make first-apply recovery crash consistent
Nickfost 4fc3f98
fix: close network policy verification gaps
Nickfost 032bed9
fix: preserve network policy trust and recovery
Nickfost 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
Some comments aren't visible on the classic Files Changed page.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,229 @@ | ||
| #!/usr/bin/env bash | ||
| # Engine stage: apply the reviewed Docker daemon network policy transactionally. | ||
| # | ||
| # Validates, drains, applies daemon.json atomically (preserving unrelated keys), | ||
| # restarts Docker ONLY via injected command boundary, runs capacity probes + | ||
| # health checks, and rolls back the exact prior config on any failure. | ||
| # | ||
| # Environment variables (all injected, never host-defaulted): | ||
| # CI_FLEET_DOCKER_DAEMON_CONFIG absolute path to daemon.json | ||
| # CI_FLEET_DOCKER_DRAIN_COMMAND path to a host drain script (runs before mutation) | ||
| # CI_FLEET_DOCKER_RESTART_COMMAND path to a Docker restart script | ||
| # CI_FLEET_DOCKER_NETWORK_PROBE path to a capacity probe script | ||
| # CI_FLEET_HEALTH_CHECK_COMMAND path to a health-check script | ||
| # CI_FLEET_COMMAND_TIMEOUT_SECONDS command timeout in seconds (default 300) | ||
| # CI_FLEET_TESTING when 1, relaxes root/strict checks | ||
| set -Eeuo pipefail | ||
|
|
||
| repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) | ||
| testing=${CI_FLEET_TESTING:-0} | ||
|
|
||
| env_file= | ||
| checkpoint_dir= | ||
|
|
||
| usage() { | ||
| cat >&2 <<'EOF' | ||
| usage: apply-docker-network-policy.sh --env PATH [--checkpoint PATH] | ||
|
|
||
| --env PATH path to the rendered ci-fleet env file (required) | ||
| --checkpoint PATH directory to back up the prior daemon.json into | ||
| EOF | ||
| } | ||
|
|
||
| die() { | ||
| printf 'ERROR: %s\n' "$*" >&2 | ||
| exit 2 | ||
| } | ||
|
|
||
| while (($#)); do | ||
| case "$1" in | ||
| --env) | ||
| [[ $# -ge 2 ]] || die '--env requires a value' | ||
| env_file=$2 | ||
| shift 2 | ||
| ;; | ||
| --checkpoint) | ||
| [[ $# -ge 2 ]] || die '--checkpoint requires a value' | ||
| checkpoint_dir=$2 | ||
| shift 2 | ||
| ;; | ||
| -h|--help) | ||
| usage | ||
| exit 0 | ||
| ;; | ||
| *) | ||
| usage | ||
| die "unknown argument: $1" | ||
| ;; | ||
| esac | ||
| done | ||
|
|
||
| [[ -n "$env_file" ]] || die '--env is required' | ||
| [[ -r "$env_file" ]] || die "rendered env is unreadable: $env_file" | ||
|
|
||
| # --- No-op when no network policy is rendered --- | ||
| count=$(awk -F= '$1 == "CI_FLEET_DOCKER_DEFAULT_ADDRESS_POOL_COUNT" {print substr($0, index($0, "=") + 1)}' "$env_file") | ||
| if [[ -z "$count" || "$count" == "0" ]]; then | ||
|
Nickfost marked this conversation as resolved.
Outdated
|
||
| printf 'NETWORK_POLICY_NOOP\n' | ||
| exit 0 | ||
|
Nickfost marked this conversation as resolved.
Outdated
|
||
| fi | ||
|
|
||
| # --- Resolve required injected commands --- | ||
| daemon_config=${CI_FLEET_DOCKER_DAEMON_CONFIG:-} | ||
| drain_command=${CI_FLEET_DOCKER_DRAIN_COMMAND:-} | ||
| restart_command=${CI_FLEET_DOCKER_RESTART_COMMAND:-} | ||
| probe_command=${CI_FLEET_DOCKER_NETWORK_PROBE:-} | ||
| health_command=${CI_FLEET_HEALTH_CHECK_COMMAND:-} | ||
| command_timeout=${CI_FLEET_COMMAND_TIMEOUT_SECONDS:-300} | ||
|
|
||
| [[ "$command_timeout" =~ ^[1-9][0-9]*$ ]] || die 'CI_FLEET_COMMAND_TIMEOUT_SECONDS must be a positive integer' | ||
| [[ -n "$daemon_config" ]] || die 'CI_FLEET_DOCKER_DAEMON_CONFIG is required when a network policy is configured' | ||
|
Nickfost marked this conversation as resolved.
|
||
| [[ -n "$restart_command" ]] || die 'CI_FLEET_DOCKER_RESTART_COMMAND is required when a network policy is configured' | ||
| [[ -n "$probe_command" ]] || die 'CI_FLEET_DOCKER_NETWORK_PROBE is required when a network policy is configured' | ||
| [[ -n "$health_command" ]] || die 'CI_FLEET_HEALTH_CHECK_COMMAND is required when a network policy is configured' | ||
| [[ -x "$restart_command" ]] || die "restart command is not executable: $restart_command" | ||
| [[ -z "$drain_command" || -x "$drain_command" ]] || die "drain command is not executable: $drain_command" | ||
|
Nickfost marked this conversation as resolved.
Outdated
|
||
| [[ -x "$probe_command" ]] || die "network probe is not executable: $probe_command" | ||
| [[ -x "$health_command" ]] || die "health-check command is not executable: $health_command" | ||
|
|
||
| # --- Ownership guard (relaxed in testing) --- | ||
| if [[ "$testing" != 1 ]]; then | ||
| [[ -w "$(dirname "$daemon_config")" ]] || die "daemon config directory is not writable: $(dirname "$daemon_config")" | ||
| if [[ -f "$daemon_config" ]]; then | ||
| file_owner=$(stat -c %u "$daemon_config") | ||
| [[ "$file_owner" == "0" ]] || die "daemon.json must be owned by root: $daemon_config" | ||
|
Nickfost marked this conversation as resolved.
Nickfost marked this conversation as resolved.
|
||
| fi | ||
| else | ||
| : # testing mode — skip root checks | ||
| fi | ||
|
|
||
| # --- Render desired daemon config block via shared validator --- | ||
| desired_pools_json=$(python3 - "$env_file" "$repo_root/scripts" <<'PY' | ||
| import json, os, sys | ||
| env_path, scripts_dir = sys.argv[1], sys.argv[2] | ||
| sys.path.insert(0, scripts_dir) | ||
| values = {} | ||
| with open(env_path, encoding="utf-8") as handle: | ||
| for line in handle: | ||
| line = line.rstrip("\n") | ||
| if "=" in line and line: | ||
| key, _, value = line.partition("=") | ||
| values[key] = value | ||
| from desired_state import render_docker_daemon_config | ||
| print(json.dumps(render_docker_daemon_config(values))) | ||
|
Nickfost marked this conversation as resolved.
Outdated
|
||
| PY | ||
| ) || die "daemon policy rendering failed" | ||
|
|
||
| # Re-check: if rendering returned empty, treat as no-op. | ||
| if [[ "$desired_pools_json" == "{}" ]]; then | ||
| printf 'NETWORK_POLICY_NOOP\n' | ||
| exit 0 | ||
| fi | ||
|
|
||
| # --- Drain before any daemon.json mutation or restart --- | ||
| if [[ -n "$drain_command" ]] && ! timeout "$command_timeout" "$drain_command" 2>&1; then | ||
| die "drain command failed before network-policy apply" | ||
| fi | ||
|
|
||
| # --- Stage merged daemon.json (preserve unrelated keys) --- | ||
| work_dir=$(mktemp -d "${CI_FLEET_TEMP_DIR:-/tmp}/.ci-fleet-apply.XXXXXX") | ||
| staging_daemon="$work_dir/daemon.json" | ||
|
|
||
| python3 - "$env_file" "$daemon_config" "$staging_daemon" "$desired_pools_json" <<'PY' || { rm -rf "$work_dir"; die "failed to stage merged daemon.json"; } | ||
| import json, os, sys | ||
| _, _, daemon_path, staging_path, desired_pools_json = sys.argv | ||
| prior = {} | ||
| if os.path.exists(daemon_path): | ||
| try: | ||
| text = open(daemon_path, encoding="utf-8").read() | ||
|
Nickfost marked this conversation as resolved.
|
||
| prior = json.loads(text) | ||
|
Nickfost marked this conversation as resolved.
Outdated
|
||
| if not isinstance(prior, dict): | ||
| raise ValueError("daemon.json root must be an object") | ||
| except (json.JSONDecodeError, ValueError) as exc: | ||
| raise SystemExit(f"ERROR: existing daemon.json is not a valid JSON object: {exc}") | ||
|
Nickfost marked this conversation as resolved.
|
||
| desired_pools = json.loads(desired_pools_json) | ||
| merged = dict(prior) | ||
| merged["default-address-pools"] = desired_pools.get("default-address-pools", []) | ||
| with open(staging_path, "w", encoding="utf-8") as handle: | ||
| json.dump(merged, handle, indent=2, sort_keys=True) | ||
| handle.write("\n") | ||
| os.chmod(staging_path, 0o644) | ||
|
Nickfost marked this conversation as resolved.
|
||
| PY | ||
|
|
||
| # --- Back up exact prior daemon.json for rollback --- | ||
| prior_daemon="$work_dir/prior" | ||
| mkdir -p "$prior_daemon" | ||
| if [[ -n "$checkpoint_dir" ]]; then | ||
| mkdir -p "$checkpoint_dir" | ||
|
Nickfost marked this conversation as resolved.
Outdated
|
||
| backup_dir="$checkpoint_dir" | ||
| backup_name="daemon.json" | ||
| else | ||
| backup_dir="$prior_daemon" | ||
| backup_name="daemon.json.before" | ||
| fi | ||
| had_prior=false | ||
| if [[ -f "$daemon_config" ]]; then | ||
| had_prior=true | ||
| cp -p "$daemon_config" "$backup_dir/$backup_name" | ||
|
Nickfost marked this conversation as resolved.
Outdated
Nickfost marked this conversation as resolved.
Outdated
|
||
| fi | ||
|
|
||
| daemon_dir=$(dirname "$daemon_config") | ||
|
|
||
| restore_daemon() { | ||
| if [[ "$had_prior" == true ]]; then | ||
| cp -p "$backup_dir/$backup_name" "$daemon_config" | ||
|
Nickfost marked this conversation as resolved.
Outdated
|
||
| else | ||
| rm -f "$daemon_config" | ||
|
Nickfost marked this conversation as resolved.
|
||
| fi | ||
| } | ||
|
|
||
| # Rollback: restore prior config, restart through the boundary, run health check. | ||
| # Failure evidence is surfaced through exit code only — no CIDRs or secrets leaked. | ||
| rollback_daemon() { | ||
| local failed=0 | ||
| restore_daemon || failed=1 | ||
| timeout "$command_timeout" "$restart_command" "$daemon_dir" >/dev/null 2>&1 || failed=1 | ||
| timeout "$command_timeout" "$health_command" >/dev/null 2>&1 || failed=1 | ||
| return "$failed" | ||
| } | ||
|
|
||
| # --- Transaction: apply → restart → probe → health, with rollback --- | ||
| # Apply daemon.json atomically (rename within same directory) | ||
| python3 - "$staging_daemon" "$daemon_dir" "$daemon_config" <<'PY' || { restore_daemon; rm -rf "$work_dir"; die "failed to apply daemon.json"; } | ||
|
Nickfost marked this conversation as resolved.
Outdated
|
||
| import os, shutil, sys, tempfile | ||
| _, _, daemon_dir, target = sys.argv | ||
| fd, tmp = tempfile.mkstemp(prefix=".daemon.json.", dir=daemon_dir) | ||
| try: | ||
| with open(sys.argv[1], "rb") as source, os.fdopen(fd, "wb") as staged: | ||
| shutil.copyfileobj(source, staged) | ||
| os.chmod(tmp, 0o644) | ||
| os.replace(tmp, target) | ||
| finally: | ||
| if os.path.exists(tmp): | ||
| os.unlink(tmp) | ||
| PY | ||
|
|
||
| # Restart Docker through the injected command boundary (never host-direct). | ||
| if ! timeout "$command_timeout" "$restart_command" "$daemon_dir" 2>&1; then | ||
|
Nickfost marked this conversation as resolved.
Outdated
|
||
| rollback_daemon || true | ||
| rm -rf "$work_dir" | ||
| die "Docker restart command failed; prior daemon.json restored" | ||
|
Nickfost marked this conversation as resolved.
Outdated
|
||
| fi | ||
|
|
||
| # Bounded capacity probe | ||
| if ! timeout "$command_timeout" "$probe_command" 2>&1; then | ||
| rollback_daemon || true | ||
| rm -rf "$work_dir" | ||
| die "capacity probe failed after network-policy restart; prior daemon.json restored" | ||
| fi | ||
|
|
||
| # Health verification | ||
| if ! timeout "$command_timeout" "$health_command" 2>&1; then | ||
| rollback_daemon || true | ||
| rm -rf "$work_dir" | ||
| die "health check failed after network-policy restart; prior daemon.json restored" | ||
| fi | ||
|
|
||
| # --- Success --- | ||
| rm -rf "$work_dir" | ||
| printf 'NETWORK_POLICY_APPLIED daemon_config=%s\n' "$daemon_config" | ||
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
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.
Uh oh!
There was an error while loading. Please reload this page.