ignore ephemeral Docker network identity in Stage 3
This commit is contained in:
parent
885dff127e
commit
1f5caf8761
5 changed files with 138 additions and 19 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
1e281c255a9a7ba72734c40a947cab4168f70b7e6b733eba7415109a40776a9d README.md
|
833809d6ce01da516fda0873d80cdbddcf6900c6aaf00be2a24201e8887c76f1 README.md
|
||||||
908ab54049c1e810ba5c47812fc51848e9eef8818a9ddc373f841672ab509a57 ROLLBACK.md
|
908ab54049c1e810ba5c47812fc51848e9eef8818a9ddc373f841672ab509a57 ROLLBACK.md
|
||||||
136707ac1391bd241efc834c549c0f54d4a849ce832c55457bdeb243cc03800a tests/50-activate-stage3.sh
|
136707ac1391bd241efc834c549c0f54d4a849ce832c55457bdeb243cc03800a tests/50-activate-stage3.sh
|
||||||
5c8eceb9b80865ae09bd4ece2a248da4677237abcc7c55d26334020543e8edf8 tests/51-rollback-stage3.sh
|
5c8eceb9b80865ae09bd4ece2a248da4677237abcc7c55d26334020543e8edf8 tests/51-rollback-stage3.sh
|
||||||
9aa43f4b72057ea28ec00309889f75e756a9b36db77e5db758b752990768c0a0 tests/52-run-stage1-inert.sh
|
9aa43f4b72057ea28ec00309889f75e756a9b36db77e5db758b752990768c0a0 tests/52-run-stage1-inert.sh
|
||||||
de70e85a4e4e6015726dfe3cbad2bdccb2990daae25eeeab272e107d2d68a115 tests/53-stage3-static-tests.sh
|
9e8ebfc21c026f7bfde07a9c4f7c190155060e52c2a792e654891a34855de84f tests/53-stage3-static-tests.sh
|
||||||
ec595556f2842329a8b1b6e88bc291ef8f4d0d98e675baeb4b317da6ae1c8e96 tests/54-stage3-failure-path-tests.sh
|
575b8ab40811daf66ba6602ff2b623acbcfc20e67b909c4f4d61f37aa3fc1790 tests/54-stage3-failure-path-tests.sh
|
||||||
ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356 tests/docker-config/config.json
|
ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356 tests/docker-config/config.json
|
||||||
1c64518e42afdf490047358c523a8213e989a84a61f51ff0126439db2b485fe3 tests/inert-bin/rm
|
1c64518e42afdf490047358c523a8213e989a84a61f51ff0126439db2b485fe3 tests/inert-bin/rm
|
||||||
034b23c09a023ce94d8b7888f0d6f45aed31ba14dbed2efd30c4b0a6322f657b tests/inert-bin/touch
|
034b23c09a023ce94d8b7888f0d6f45aed31ba14dbed2efd30c4b0a6322f657b tests/inert-bin/touch
|
||||||
3a3a84df1021e02cc258edbee621ab8abfb9f83bdc17dfa689fb9823b17c8cfd tests/stage3-common.sh
|
95a5241890b841f5838dc37b364f27d8c00eb0324e718a615946d14d946bbaba tests/stage3-common.sh
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ Before any mutation, activation requires:
|
||||||
- fresh immediate fingerprints of nonproject nftables, stable nonproject host network state, rootful Docker networks/runtime/ports, host listeners, resolver metadata, and public IPv4; and
|
- fresh immediate fingerprints of nonproject nftables, stable nonproject host network state, rootful Docker networks/runtime/ports, host listeners, resolver metadata, and public IPv4; and
|
||||||
- no marker, namespace, project veth/table, user manager, linger, UID-1200 process, or rootless socket.
|
- no marker, namespace, project veth/table, user manager, linger, UID-1200 process, or rootless socket.
|
||||||
|
|
||||||
|
The 41-network check uses one deterministic, name-sorted policy canonical for committed preflight, fresh-baseline capture, activation provenance, automatic recovery, and explicit rollback. It requires the exact unique name set and count; driver, scope, `Internal`, `Attachable`, `Ingress`, `EnableIPv6`, and `ConfigOnly`; normalized IPAM driver/options and every subnet, gateway, IP range, and auxiliary address; and network driver options that affect routing or isolation. Docker-generated network IDs, creation timestamps, container/endpoint attachments, peers, status, and other runtime bookkeeping are excluded. The separate denial gate still requires the exact committed set of 39 unique IPv4 Docker gateways.
|
||||||
|
|
||||||
Immediately before publication and start, the script revalidates the loaded manager state: `systemd-analyze verify`; exact `systemctl cat` fragment/drop-in surfaces for both units; exact namespace fragment path with no drop-ins; current manager cache; no pending job; exact single `ExecStart`/`ExecStop`; oneshot/`RemainAfterExit`; static inactive state; the approval-marker condition; `Before=user@1200.service`; and the reciprocal user-manager `Requires`/`After`, namespace path, fragment, and exact two-drop-in set.
|
Immediately before publication and start, the script revalidates the loaded manager state: `systemd-analyze verify`; exact `systemctl cat` fragment/drop-in surfaces for both units; exact namespace fragment path with no drop-ins; current manager cache; no pending job; exact single `ExecStart`/`ExecStop`; oneshot/`RemainAfterExit`; static inactive state; the approval-marker condition; `Before=user@1200.service`; and the reciprocal user-manager `Requires`/`After`, namespace path, fragment, and exact two-drop-in set.
|
||||||
|
|
||||||
The script renders and fsyncs a root-owned mode `0600`, single-link version-2 record before atomically renaming it into the approval-marker path and syncing the parent directory. No hard-link publication window exists, and an interrupted write cannot replace the prior authoritative record. Parsing requires the exact canonical one-delimiter, final-newline-complete byte representation, so trailing fields, truncated final writes, and embedded NUL bytes are rejected. The immutable fields include an activation UUID, machine-ID digest, approved Quad9 pair, Stage 1 source and artifact digest, Stage 2 verification commit, Stage 3 activation commit, UTC timestamp, hostname, boot ID, public IPv4, installed-policy digest, and baseline fingerprints. It starts `le-app-codex-netns.service`, then atomically replaces `PREPARED` with an `ACTIVE` record bound to the service invocation, namespace inode, host-veth ifindex/iflink/MAC, both nft table handles, independent counter-normalized table digests, and the combined project-policy digest.
|
The script renders and fsyncs a root-owned mode `0600`, single-link version-2 record before atomically renaming it into the approval-marker path and syncing the parent directory. No hard-link publication window exists, and an interrupted write cannot replace the prior authoritative record. Parsing requires the exact canonical one-delimiter, final-newline-complete byte representation, so trailing fields, truncated final writes, and embedded NUL bytes are rejected. The immutable fields include an activation UUID, machine-ID digest, approved Quad9 pair, Stage 1 source and artifact digest, Stage 2 verification commit, Stage 3 activation commit, UTC timestamp, hostname, boot ID, public IPv4, installed-policy digest, and baseline fingerprints. It starts `le-app-codex-netns.service`, then atomically replaces `PREPARED` with an `ACTIVE` record bound to the service invocation, namespace inode, host-veth ifindex/iflink/MAC, both nft table handles, independent counter-normalized table digests, and the combined project-policy digest.
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ if /usr/bin/grep -Eq '/usr/bin/nft[[:space:]]+--numeric|/usr/bin/nft[[:space:]]+
|
||||||
[ "$(/usr/bin/grep -Fc 'stage3_nft_json_inet_rules_match "$raw"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_nft_json_nat_rules_match "$raw"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_project_inet_snapshot_valid "$raw" "$text"' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_project_nat_snapshot_valid "$raw" "$text"' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_capture_exact_project_policy_snapshot' "$activate")" -eq 4 ] && ! /usr/bin/grep -q 'stage3_nft_text_chain' "$activate" "$rollback" "$common" && pass 'activation and rollback share exact canonical JSON policy validation' || fail 'nft JSON validation surface'
|
[ "$(/usr/bin/grep -Fc 'stage3_nft_json_inet_rules_match "$raw"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_nft_json_nat_rules_match "$raw"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_project_inet_snapshot_valid "$raw" "$text"' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_project_nat_snapshot_valid "$raw" "$text"' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_capture_exact_project_policy_snapshot' "$activate")" -eq 4 ] && ! /usr/bin/grep -q 'stage3_nft_text_chain' "$activate" "$rollback" "$common" && pass 'activation and rollback share exact canonical JSON policy validation' || fail 'nft JSON validation surface'
|
||||||
[ "$(/usr/bin/grep -Fc '/usr/bin/nft delete table inet le_app_codex' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/nft delete table ip le_app_codex_nat' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/ip link delete lecodex-host' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/ip netns delete le-app-codex' "$rollback")" -eq 1 ] && pass 'rollback direct cleanup limited to exact project names' || fail 'rollback direct cleanup literals'
|
[ "$(/usr/bin/grep -Fc '/usr/bin/nft delete table inet le_app_codex' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/nft delete table ip le_app_codex_nat' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/ip link delete lecodex-host' "$rollback")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/ip netns delete le-app-codex' "$rollback")" -eq 1 ] && pass 'rollback direct cleanup limited to exact project names' || fail 'rollback direct cleanup literals'
|
||||||
if [ "$(/usr/bin/grep -Fc '/usr/bin/docker --config "$stage3_root/tests/docker-config" --host unix:///run/docker.sock "$@"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/docker' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker ' "$common")" -eq 4 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker network ls --no-trunc -q' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker network inspect --format' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker ps --no-trunc -aq' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker inspect --format' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc -- '-u DOCKER_API_VERSION -u DOCKER_CERT_PATH -u DOCKER_CONTEXT -u DOCKER_HOST -u DOCKER_TLS -u DOCKER_TLS_VERIFY' "$common")" -eq 1 ] && /usr/bin/jq -e 'type == "object" and length == 0' "$stage3_root/tests/docker-config/config.json" >/dev/null; then pass 'all rootful Docker reads use one context-scrubbed explicit-socket read allowlist and empty config'; else fail 'rootful Docker helper/socket/config surface'; fi
|
if [ "$(/usr/bin/grep -Fc '/usr/bin/docker --config "$stage3_root/tests/docker-config" --host unix:///run/docker.sock "$@"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/usr/bin/docker' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker ' "$common")" -eq 4 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker network ls --no-trunc -q' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker network inspect --format' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker ps --no-trunc -aq' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_rootful_docker inspect --format' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc -- '-u DOCKER_API_VERSION -u DOCKER_CERT_PATH -u DOCKER_CONTEXT -u DOCKER_HOST -u DOCKER_TLS -u DOCKER_TLS_VERIFY' "$common")" -eq 1 ] && /usr/bin/jq -e 'type == "object" and length == 0' "$stage3_root/tests/docker-config/config.json" >/dev/null; then pass 'all rootful Docker reads use one context-scrubbed explicit-socket read allowlist and empty config'; else fail 'rootful Docker helper/socket/config surface'; fi
|
||||||
|
[ "$(/usr/bin/grep -Fc 'stage3_docker_network_policy_canonical' "$common")" -eq 3 ] && [ "$(/usr/bin/grep -Fc '}) | sort_by(.Name)' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_docker_network_policy_canonical < "$stage1_root/inventory/docker-networks.json"' "$common")" -eq 1 ] && [ "$(/usr/bin/grep -Fc "case_docker_network_policy_canonical" "$failure_suite")" -eq 2 ] && pass 'one name-sorted Docker policy canonical feeds live, committed, baseline, and rollback comparisons' || fail 'Docker network policy canonical surface'
|
||||||
[ "$(/usr/bin/grep -Fc '/usr/bin/systemd-run --service-type=exec --wait --pipe --collect --unit="$inert_unit"' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/srv/le-app-codex/phase2b-tests/10-inert-containment.sh' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc -- '--property=RuntimeMaxSec=120s' "$runner")" -eq 1 ] && pass 'finite collected wrapper executes existing installed inert test' || fail 'inert test wrapper command surface'
|
[ "$(/usr/bin/grep -Fc '/usr/bin/systemd-run --service-type=exec --wait --pipe --collect --unit="$inert_unit"' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '/srv/le-app-codex/phase2b-tests/10-inert-containment.sh' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc -- '--property=RuntimeMaxSec=120s' "$runner")" -eq 1 ] && pass 'finite collected wrapper executes existing installed inert test' || fail 'inert test wrapper command surface'
|
||||||
[ "$(/usr/bin/grep -Fc -- "--property='UnsetEnvironment=ALL_PROXY HTTPS_PROXY HTTP_PROXY NO_PROXY all_proxy https_proxy http_proxy no_proxy'" "$runner")" -eq 1 ] && ! /usr/bin/grep -Eq -- '--setenv=(ALL_PROXY|HTTPS_PROXY|HTTP_PROXY|NO_PROXY|all_proxy|https_proxy|http_proxy|no_proxy)' "$runner" && pass 'transient inert test removes every proxy variable' || fail 'transient inert proxy environment'
|
[ "$(/usr/bin/grep -Fc -- "--property='UnsetEnvironment=ALL_PROXY HTTPS_PROXY HTTP_PROXY NO_PROXY all_proxy https_proxy http_proxy no_proxy'" "$runner")" -eq 1 ] && ! /usr/bin/grep -Eq -- '--setenv=(ALL_PROXY|HTTPS_PROXY|HTTP_PROXY|NO_PROXY|all_proxy|https_proxy|http_proxy|no_proxy)' "$runner" && pass 'transient inert test removes every proxy variable' || fail 'transient inert proxy environment'
|
||||||
[ "$(/usr/bin/grep -Fc -- '--setenv="PHASE2B_STAGE3_INERT_TOKEN=$inert_token"' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '[ "$inert_owned" -eq 1 ] && [[ "$inert_invocation" =~ ^[0-9a-f]{32}$ ]]' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_inert_unit_identity "$show" "$inert_unit" "${inert_invocation:-}" "$inert_activation" "$inert_token"' "$runner")" -eq 1 ] && pass 'transient success and cleanup require token-bound captured ownership' || fail 'transient ownership/token gate'
|
[ "$(/usr/bin/grep -Fc -- '--setenv="PHASE2B_STAGE3_INERT_TOKEN=$inert_token"' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc '[ "$inert_owned" -eq 1 ] && [[ "$inert_invocation" =~ ^[0-9a-f]{32}$ ]]' "$runner")" -eq 1 ] && [ "$(/usr/bin/grep -Fc 'stage3_inert_unit_identity "$show" "$inert_unit" "${inert_invocation:-}" "$inert_activation" "$inert_token"' "$runner")" -eq 1 ] && pass 'transient success and cleanup require token-bound captured ownership' || fail 'transient ownership/token gate'
|
||||||
|
|
@ -112,7 +113,14 @@ if /usr/bin/awk '
|
||||||
failures_before=$failures
|
failures_before=$failures
|
||||||
source "$common"
|
source "$common"
|
||||||
committed_networks=$(stage3_committed_docker_network_canonical)
|
committed_networks=$(stage3_committed_docker_network_canonical)
|
||||||
printf '%s' "$committed_networks" | /usr/bin/jq -e 'length == 41' >/dev/null && pass 'committed Docker canonicalizer yields 41 networks' || fail 'committed Docker canonicalizer'
|
printf '%s' "$committed_networks" | /usr/bin/jq -e '
|
||||||
|
length == 41 and ([.[].Name] | unique | length) == 41 and
|
||||||
|
all(.[];
|
||||||
|
keys == ["Attachable","ConfigOnly","Driver","EnableIPv6","IPAM","Ingress","Internal","Name","Options","Scope"] and
|
||||||
|
(.Options | type) == "object" and
|
||||||
|
(.IPAM | keys) == ["Config","Driver","Options"] and
|
||||||
|
(.IPAM.Options | type) == "object" and (.IPAM.Config | type) == "array" and
|
||||||
|
all(.IPAM.Config[]; keys == ["AuxiliaryAddresses","Gateway","IPRange","Subnet"] and (.AuxiliaryAddresses | type) == "object"))' >/dev/null && pass 'committed Docker policy canonical yields exact 41-name inventory' || fail 'committed Docker policy canonical'
|
||||||
[ "$failures" -eq "$failures_before" ] || fail 'common library source has side effects'
|
[ "$failures" -eq "$failures_before" ] || fail 'common library source has side effects'
|
||||||
|
|
||||||
/bin/bash "$stage1_root/tests/32-stage1-state-tests.sh" >/dev/null && pass 'existing non-mutating Stage 1 state tests' || fail 'Stage 1 state tests'
|
/bin/bash "$stage1_root/tests/32-stage1-state-tests.sh" >/dev/null && pass 'existing non-mutating Stage 1 state tests' || fail 'Stage 1 state tests'
|
||||||
|
|
|
||||||
|
|
@ -418,7 +418,7 @@ case_malformed_and_escaping_records() {
|
||||||
}
|
}
|
||||||
|
|
||||||
case_host_and_repository_provenance_mismatch() {
|
case_host_and_repository_provenance_mismatch() {
|
||||||
local root=$fixture/provenance good bad head parent original_stage3_root
|
local root=$fixture/provenance good bad head parent
|
||||||
root=$fixture/provenance; good=$root/good; bad=$root/bad
|
root=$fixture/provenance; good=$root/good; bad=$root/bad
|
||||||
/usr/bin/mkdir "$root"
|
/usr/bin/mkdir "$root"
|
||||||
head=$(/usr/bin/git -C "$repo_root" rev-parse HEAD) || return 1
|
head=$(/usr/bin/git -C "$repo_root" rev-parse HEAD) || return 1
|
||||||
|
|
@ -442,13 +442,10 @@ case_host_and_repository_provenance_mismatch() {
|
||||||
! stage3_parse_record_file "$bad" 1 || return 1
|
! stage3_parse_record_file "$bad" 1 || return 1
|
||||||
/usr/bin/sed "s/^STAGE3_ACTIVATION_COMMIT|.*/STAGE3_ACTIVATION_COMMIT|$(printf '0%.0s' {1..40})/" "$good" > "$bad"
|
/usr/bin/sed "s/^STAGE3_ACTIVATION_COMMIT|.*/STAGE3_ACTIVATION_COMMIT|$(printf '0%.0s' {1..40})/" "$good" > "$bad"
|
||||||
! stage3_parse_record_file "$bad" 1 || return 1
|
! stage3_parse_record_file "$bad" 1 || return 1
|
||||||
original_stage3_root=$stage3_root
|
|
||||||
stage3_root=$stage2_root
|
|
||||||
stage3_head=$parent
|
stage3_head=$parent
|
||||||
stage3_render_record PREPARED > "$bad"; /usr/bin/chmod 0600 "$bad"
|
stage3_render_record PREPARED > "$bad"; /usr/bin/chmod 0600 "$bad"
|
||||||
stage3_head=$head
|
stage3_head=$head
|
||||||
! stage3_parse_record_file "$bad" 1 || return 1
|
! stage3_parse_record_file "$bad" 1 || return 1
|
||||||
stage3_root=$original_stage3_root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case_lock_mismatch_and_concurrency() {
|
case_lock_mismatch_and_concurrency() {
|
||||||
|
|
@ -563,6 +560,71 @@ case_docker_gateway_inventory() {
|
||||||
! stage3_materialize_docker_ipv4_gateways '[]' && [ "${#stage3_docker_gateways[@]}" -eq 0 ]
|
! stage3_materialize_docker_ipv4_gateways '[]' && [ "${#stage3_docker_gateways[@]}" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case_docker_network_policy_canonical() {
|
||||||
|
local raw baseline changed alternate canonical alternate_canonical filter
|
||||||
|
local old_id=4ecbee63f4b0f705479ec65b74b57c618b61d76da91bc5c042717460709dadb8
|
||||||
|
local new_id=575a3269b7cf00a0e79b3e3f1e2a7c5b8159b125792704b2f6044a38658493d4
|
||||||
|
local -a policy_mutations=(
|
||||||
|
'map(if .Name == "immich_s6_net" then .Name = "immich_s6_net_changed" else . end)'
|
||||||
|
'map(select(.Name != "immich_s6_net"))'
|
||||||
|
'. + [(.[] | select(.Name == "immich_s6_net") | .Name = "immich_s6_net_added")]'
|
||||||
|
'map(if .Name == "immich_s6_net" then .Driver = "overlay" else . end)'
|
||||||
|
'map(if .Name == "immich_s6_net" then .Scope = "swarm" else . end)'
|
||||||
|
'map(if .Name == "immich_s6_net" then .Internal = (.Internal | not) else . end)'
|
||||||
|
'map(if .Name == "immich_s6_net" then .Attachable = (.Attachable | not) else . end)'
|
||||||
|
'map(if .Name == "immich_s6_net" then .Ingress = (.Ingress | not) else . end)'
|
||||||
|
'map(if .Name == "immich_s6_net" then .EnableIPv6 = (.EnableIPv6 | not) else . end)'
|
||||||
|
'map(if .Name == "immich_s6_net" then .ConfigOnly = (.ConfigOnly | not) else . end)'
|
||||||
|
'map(if .Name == "immich_s6_net" then .IPAM.Driver = "changed" else . end)'
|
||||||
|
'map(if .Name == "immich_s6_net" then .IPAM.Options = {"fixture":"changed"} else . end)'
|
||||||
|
'map(if .Name == "immich_s6_net" then .IPAM.Config[0].Subnet = "10.254.0.0/24" else . end)'
|
||||||
|
'map(if .Name == "immich_s6_net" then .IPAM.Config[0].Gateway = "10.254.0.1" else . end)'
|
||||||
|
'map(if .Name == "immich_s6_net" then .IPAM.Config[0].IPRange = "10.18.128.0/17" else . end)'
|
||||||
|
'map(if .Name == "immich_s6_net" then .IPAM.Config[0].AuxiliaryAddresses = {"fixture":"10.18.0.254"} else . end)'
|
||||||
|
'map(if .Name == "immich_s6_net" then .Options = {"com.docker.network.bridge.enable_icc":"false"} else . end)'
|
||||||
|
)
|
||||||
|
|
||||||
|
raw=$(/usr/bin/cat "$stage1_root/inventory/docker-networks.json") || return 1
|
||||||
|
[ "$(printf '%s' "$raw" | /usr/bin/jq -r '.[] | select(.Name == "immich_s6_net") | .Id')" = "$old_id" ] || return 1
|
||||||
|
baseline=$(printf '%s' "$raw" | stage3_docker_network_policy_canonical) || return 1
|
||||||
|
printf '%s' "$baseline" | /usr/bin/jq -e 'length == 41 and ([.[].Name] | unique | length) == 41' >/dev/null || return 1
|
||||||
|
|
||||||
|
changed=$(printf '%s' "$raw" | /usr/bin/jq -c --arg id "$new_id" 'map(if .Name == "immich_s6_net" then .Id = $id else . end)') || return 1
|
||||||
|
canonical=$(printf '%s' "$changed" | stage3_docker_network_policy_canonical) || return 1
|
||||||
|
[ "$canonical" = "$baseline" ] || return 1
|
||||||
|
|
||||||
|
changed=$(printf '%s' "$raw" | /usr/bin/jq -c '
|
||||||
|
map(if .Name == "immich_s6_net" then
|
||||||
|
.Created = "2099-01-01T00:00:00Z" |
|
||||||
|
.Containers = {"runtime-container":{"Name":"runtime","EndpointID":"runtime-endpoint"}} |
|
||||||
|
.Peers = [{"Name":"runtime-peer"}] |
|
||||||
|
.Status = {"runtime":"changed"}
|
||||||
|
else . end)') || return 1
|
||||||
|
canonical=$(printf '%s' "$changed" | stage3_docker_network_policy_canonical) || return 1
|
||||||
|
[ "$canonical" = "$baseline" ] || return 1
|
||||||
|
|
||||||
|
changed=$(printf '%s' "$raw" | /usr/bin/jq -c 'reverse | map(if .Name == "immich_s6_net" then .IPAM.Config |= reverse else . end)') || return 1
|
||||||
|
canonical=$(printf '%s' "$changed" | stage3_docker_network_policy_canonical) || return 1
|
||||||
|
[ "$canonical" = "$baseline" ] || return 1
|
||||||
|
|
||||||
|
changed=$(printf '%s' "$raw" | /usr/bin/jq -c 'map(if .Name == "immich_s6_net" then .IPAM.Config[0].AuxiliaryAddresses = {"z":"10.18.0.253","a":"10.18.0.254"} else . end)') || return 1
|
||||||
|
alternate=$(printf '%s' "$raw" | /usr/bin/jq -c 'map(if .Name == "immich_s6_net" then .IPAM.Config[0].AuxiliaryAddresses = {"a":"10.18.0.254","z":"10.18.0.253"} else . end)') || return 1
|
||||||
|
canonical=$(printf '%s' "$changed" | stage3_docker_network_policy_canonical) || return 1
|
||||||
|
alternate_canonical=$(printf '%s' "$alternate" | stage3_docker_network_policy_canonical) || return 1
|
||||||
|
[ "$canonical" = "$alternate_canonical" ] || return 1
|
||||||
|
|
||||||
|
for filter in "${policy_mutations[@]}"; do
|
||||||
|
changed=$(printf '%s' "$raw" | /usr/bin/jq -c "$filter") || return 1
|
||||||
|
canonical=$(printf '%s' "$changed" | stage3_docker_network_policy_canonical) || return 1
|
||||||
|
[ "$canonical" != "$baseline" ] || return 1
|
||||||
|
done
|
||||||
|
|
||||||
|
changed=$(printf '%s' "$raw" | /usr/bin/jq -c '.[1].Name = .[0].Name') || return 1
|
||||||
|
! printf '%s' "$changed" | stage3_docker_network_policy_canonical >/dev/null 2>&1 || return 1
|
||||||
|
changed=$(printf '%s' "$raw" | /usr/bin/jq -c 'del(.[0].EnableIPv6)') || return 1
|
||||||
|
! printf '%s' "$changed" | stage3_docker_network_policy_canonical >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
case_veth_drift() {
|
case_veth_drift() {
|
||||||
stage3_veth_fields_match 10 11 aa:bb:cc:dd:ee:ff 10 11 aa:bb:cc:dd:ee:ff || return 1
|
stage3_veth_fields_match 10 11 aa:bb:cc:dd:ee:ff 10 11 aa:bb:cc:dd:ee:ff || return 1
|
||||||
! stage3_veth_fields_match 10 11 aa:bb:cc:dd:ee:ff 12 11 aa:bb:cc:dd:ee:ff || return 1
|
! stage3_veth_fields_match 10 11 aa:bb:cc:dd:ee:ff 12 11 aa:bb:cc:dd:ee:ff || return 1
|
||||||
|
|
@ -623,6 +685,7 @@ expect_success 'transient unit collision and wrong ownership token rejected' cas
|
||||||
expect_success 'inert signals are deferred through ownership capture and PID start-time identity is exact' case_inert_signal_deferral_and_pid_identity
|
expect_success 'inert signals are deferred through ownership capture and PID start-time identity is exact' case_inert_signal_deferral_and_pid_identity
|
||||||
expect_success 'inert write probe cleaned exactly and symlink preserved' case_inert_probe_cleanup
|
expect_success 'inert write probe cleaned exactly and symlink preserved' case_inert_probe_cleanup
|
||||||
expect_success 'actual inert cleanup function removes mocked unit/probe and propagates execution or cleanup failure' case_inert_probe_failure_trap
|
expect_success 'actual inert cleanup function removes mocked unit/probe and propagates execution or cleanup failure' case_inert_probe_failure_trap
|
||||||
|
expect_success 'Docker network policy canonical ignores runtime identity churn and rejects policy drift' case_docker_network_policy_canonical
|
||||||
expect_success 'Docker gateway inventory requires exact 39 unique addresses' case_docker_gateway_inventory
|
expect_success 'Docker gateway inventory requires exact 39 unique addresses' case_docker_gateway_inventory
|
||||||
expect_success 'veth identity drift rejected before cleanup' case_veth_drift
|
expect_success 'veth identity drift rejected before cleanup' case_veth_drift
|
||||||
expect_success 'table/veth drift aborts each direct cleanup path before deletion' case_direct_cleanup_drift_gates
|
expect_success 'table/veth drift aborts each direct cleanup path before deletion' case_direct_cleanup_drift_gates
|
||||||
|
|
|
||||||
|
|
@ -202,8 +202,58 @@ stage3_rootful_docker() {
|
||||||
/usr/bin/docker --config "$stage3_root/tests/docker-config" --host unix:///run/docker.sock "$@"
|
/usr/bin/docker --config "$stage3_root/tests/docker-config" --host unix:///run/docker.sock "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stage3_docker_network_policy_canonical() {
|
||||||
|
/usr/bin/jq -eS -c '
|
||||||
|
def nullable_string: type == "null" or type == "string";
|
||||||
|
def string_map: type == "object" and all(to_entries[]; (.key | type) == "string" and (.value | type) == "string");
|
||||||
|
def nullable_string_map: type == "null" or string_map;
|
||||||
|
def normalized_string_map: if . == null then {} else to_entries | sort_by(.key) | from_entries end;
|
||||||
|
def ipam_config:
|
||||||
|
type == "object" and
|
||||||
|
((.Subnet // null) | nullable_string) and
|
||||||
|
((.Gateway // null) | nullable_string) and
|
||||||
|
((.IPRange // null) | nullable_string) and
|
||||||
|
((.AuxiliaryAddresses // null) | nullable_string_map);
|
||||||
|
length as $count |
|
||||||
|
if type == "array" and $count > 0 and
|
||||||
|
all(.[];
|
||||||
|
type == "object" and
|
||||||
|
(.Name | type) == "string" and (.Driver | type) == "string" and (.Scope | type) == "string" and
|
||||||
|
(.Internal | type) == "boolean" and (.Attachable | type) == "boolean" and
|
||||||
|
(.Ingress | type) == "boolean" and (.EnableIPv6 | type) == "boolean" and
|
||||||
|
(.ConfigOnly | type) == "boolean" and (.IPAM | type) == "object" and
|
||||||
|
(.IPAM.Driver | type) == "string" and (.IPAM.Options | nullable_string_map) and
|
||||||
|
((.IPAM.Config // []) | type == "array" and all(.[]; ipam_config)) and
|
||||||
|
((.Options // {}) | string_map)) and
|
||||||
|
([.[].Name] | unique | length) == $count
|
||||||
|
then map({
|
||||||
|
Name,
|
||||||
|
Driver,
|
||||||
|
Scope,
|
||||||
|
Internal,
|
||||||
|
Attachable,
|
||||||
|
Ingress,
|
||||||
|
EnableIPv6,
|
||||||
|
ConfigOnly,
|
||||||
|
IPAM: {
|
||||||
|
Driver: .IPAM.Driver,
|
||||||
|
Options: (.IPAM.Options | normalized_string_map),
|
||||||
|
Config: ((.IPAM.Config // []) |
|
||||||
|
map({
|
||||||
|
Subnet: (.Subnet // null),
|
||||||
|
Gateway: (.Gateway // null),
|
||||||
|
IPRange: (.IPRange // null),
|
||||||
|
AuxiliaryAddresses: (.AuxiliaryAddresses | normalized_string_map)
|
||||||
|
}) |
|
||||||
|
sort_by([.Subnet // "", .Gateway // "", .IPRange // "", (.AuxiliaryAddresses | tojson)]))
|
||||||
|
},
|
||||||
|
Options: (.Options | normalized_string_map)
|
||||||
|
}) | sort_by(.Name)
|
||||||
|
else error("invalid Docker network policy inventory") end'
|
||||||
|
}
|
||||||
|
|
||||||
stage3_docker_network_canonical() {
|
stage3_docker_network_canonical() {
|
||||||
local ids_text raw id
|
local ids_text raw canonical id
|
||||||
local -a ids=()
|
local -a ids=()
|
||||||
ids_text=$(stage3_rootful_docker network ls --no-trunc -q 2>/dev/null) || return 1
|
ids_text=$(stage3_rootful_docker network ls --no-trunc -q 2>/dev/null) || return 1
|
||||||
[ -n "$ids_text" ] || return 1
|
[ -n "$ids_text" ] || return 1
|
||||||
|
|
@ -212,18 +262,14 @@ stage3_docker_network_canonical() {
|
||||||
ids+=("$id")
|
ids+=("$id")
|
||||||
done <<< "$ids_text"
|
done <<< "$ids_text"
|
||||||
[ "${#ids[@]}" -gt 0 ] || return 1
|
[ "${#ids[@]}" -gt 0 ] || return 1
|
||||||
raw=$(stage3_rootful_docker network inspect --format '{"Name":{{json .Name}},"Id":{{json .Id}},"Scope":{{json .Scope}},"Driver":{{json .Driver}},"EnableIPv4":{{json .EnableIPv4}},"EnableIPv6":{{json .EnableIPv6}},"IPAM":{{json .IPAM}},"Internal":{{json .Internal}},"Attachable":{{json .Attachable}},"Ingress":{{json .Ingress}}}' "${ids[@]}" 2>/dev/null) || return 1
|
raw=$(stage3_rootful_docker network inspect --format '{"Name":{{json .Name}},"Scope":{{json .Scope}},"Driver":{{json .Driver}},"EnableIPv6":{{json .EnableIPv6}},"IPAM":{{json .IPAM}},"Internal":{{json .Internal}},"Attachable":{{json .Attachable}},"Ingress":{{json .Ingress}},"ConfigOnly":{{json .ConfigOnly}},"Options":{{json .Options}}}' "${ids[@]}" 2>/dev/null) || return 1
|
||||||
printf '%s\n' "$raw" | /usr/bin/jq -esS -c '
|
canonical=$(printf '%s\n' "$raw" | /usr/bin/jq -s -c '.' | stage3_docker_network_policy_canonical) || return 1
|
||||||
if length > 0 and all(.[];
|
[ "$(printf '%s' "$canonical" | /usr/bin/jq 'length')" -eq "${#ids[@]}" ] || return 1
|
||||||
type == "object" and (.Name | type) == "string" and (.Id | type) == "string" and
|
printf '%s\n' "$canonical"
|
||||||
(.Scope | type) == "string" and (.Driver | type) == "string" and (.IPAM | type) == "object" and
|
|
||||||
(.EnableIPv4 | type) == "boolean" and (.EnableIPv6 | type) == "boolean" and
|
|
||||||
(.Internal | type) == "boolean" and (.Attachable | type) == "boolean" and (.Ingress | type) == "boolean")
|
|
||||||
then sort_by(.Id) else error("invalid Docker network inventory") end'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stage3_committed_docker_network_canonical() {
|
stage3_committed_docker_network_canonical() {
|
||||||
/usr/bin/jq -S -c 'map({Name,Id,Scope,Driver,EnableIPv4,EnableIPv6,IPAM,Internal,Attachable,Ingress}) | sort_by(.Id)' "$stage1_root/inventory/docker-networks.json"
|
stage3_docker_network_policy_canonical < "$stage1_root/inventory/docker-networks.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
stage3_nonproject_nft_canonical() {
|
stage3_nonproject_nft_canonical() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue