Skip to content

feat: define Docker network capacity policy - #82

Merged
Nickfost merged 8 commits into
mainfrom
feat/issue-81-network-policy
Aug 27, 2026
Merged

feat: define Docker network capacity policy#82
Nickfost merged 8 commits into
mainfrom
feat/issue-81-network-policy

Conversation

@Nickfost

@Nickfost Nickfost commented Aug 26, 2026

Copy link
Copy Markdown
Member

Summary

  • define and validate reviewed Docker network-capacity policy in schema-v3 desired state
  • render policy only for read-only health inspection
  • report aggregate subnet headroom, exhaustion, inspection failures, and legacy networks without exposing addresses
  • preserve compatibility with legacy rendered state that predates this policy

This is phase 1 of #81. It detects and reports risk only.

Validation

  • python3 scripts/test_desired_state.py — 17 passed
  • python3 scripts/test_health.py — 25 passed
  • python3 templates/config-repository/scripts/test_policy.py — 64 passed
  • python3 scripts/test_status_receiver.py — 28 passed
  • scripts/validate.sh — all non-Docker gates passed, including installer and status receiver installation tests; stopped at the final Docker command because the local environment has no Docker CLI

Excluded later #81 slices

  • controller circuit breaking
  • frequent orphan reconciliation
  • Docker daemon configuration mutation or restart
  • transactional exhausted-pool recovery
  • automatic cleanup, network pruning, or runner draining
  • controller scaling changes
  • downstream-consumer label changes
  • infrastructure deployment or desired-state rollout

Merging this PR does not authorize deployment, host access, Docker cleanup or restart, desired-state rollout, VM changes, or any other infrastructure action.

@Nickfost

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a7898c292

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/health.py Outdated
Comment on lines +271 to +274
"network": {
key: snapshot.get("docker_network_headroom", {}).get(key, 0)
for key in ("configured", "used", "free", "legacy")
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Accept network metrics in the status receiver

When status reporting is enabled, every report now contains docker.network, but StatusReceiver._validate_minimum still requires the Docker object to contain exactly healthy and oom (scripts/status_receiver.py:242). Consequently the receiver rejects every report from this engine as invalid_report, leaving external controller status stale; update the receiver validation and its compatibility tests alongside the sender.

Useful? React with 👍 / 👎.

Comment thread scripts/health.py
Comment thread scripts/health.py Outdated
Comment on lines +399 to +401
for subnet in subnets:
if any(subnet.subnet_of(pool) for pool in pools):
used_subnets.add(str(subnet))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count occupied allocation-size slots

When an inspected subnet is inside a configured base but has a broader prefix than the policy's allocation size, this adds only one set entry. For example, a /24 network inside a /24 pool configured with size: 28 occupies all 16 allocatable /28 slots, yet the report says used=1, free=15, and healthy. Count the allocation-size subnets covered by each network, or classify mismatched prefixes as nonconforming, so exhaustion is not hidden.

Useful? React with 👍 / 👎.

Comment thread scripts/health.py Outdated
Comment on lines +392 to +393
if network.version != 4:
return empty

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore IPv6 entries during IPv4 pool inspection

When a Docker network is dual-stack, its valid IPv6 IPAM entry reaches this condition and causes the entire IPv4 headroom inspection to return unavailable, which evaluate reports as critical. Since the configured policy explicitly measures IPv4 pools, IPv6 entries should be skipped while retaining and evaluating the network's IPv4 subnets.

Useful? React with 👍 / 👎.

"type": "object",
"additionalProperties": false,
"required": ["pool", "location", "state", "scale_set_name", "lifecycle", "engine_ref", "min_runners", "max_runners", "runner_resources"],
"required": ["pool", "location", "state", "scale_set_name", "lifecycle", "engine_ref", "min_runners", "max_runners", "runner_resources", "docker_network_policy"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve an upgrade path from the previous schema-v3 engine

Making docker_network_policy immediately required leaves no configuration that both the previous and new installed managers accept: the previous exact-key validator rejects the new field as unknown, while this validator rejects a configuration without it. The installed manager's remote-reconcile.sh validates desired state with its current engine before fetching the newly pinned engine, so existing managed controllers cannot converge to this revision through the authoritative desired-state lifecycle; stage the field as optional/capability-gated before making it mandatory.

AGENTS.md reference: AGENTS.md:L45-L45

Useful? React with 👍 / 👎.

Comment thread templates/config-repository/scripts/init.py
@Nickfost

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bbe3be5c49

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/health.py Outdated
Comment on lines +399 to +402
match = next(((pool, size) for pool, size in pools if subnet.subnet_of(pool)), None)
if match is None:
network_legacy = True
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count overlaps that contain an address pool

When an existing Docker subnet contains a configured pool, subnet.subnet_of(pool) is false, so the subnet is only marked legacy and none of the overlapping allocation slots are counted. For example, a 198.51.100.0/23 network covering a configured 198.51.100.0/24 pool with /28 allocations is reported as used=0, free=16, warning, although every configured slot is occupied and exhaustion should be critical. Fresh evidence in the reviewed final tree is that the new containment check still excludes this inverse-overlap case; count the configured slots in every intersection.

Useful? React with 👍 / 👎.

Comment thread scripts/health.py Outdated
Comment on lines +404 to +405
if subnet.prefixlen <= size:
used_subnets.update(str(slot) for slot in subnet.subnets(new_prefix=size))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid materializing every allocation slot

For accepted policies with a large prefix gap, this expands every covered allocation into a string and retains it in a set. A valid /8 pool with size: 32 and an inspected /8 network attempts to create roughly 16.8 million strings during each health check, consuming gigabytes of memory and potentially killing or indefinitely delaying monitoring. Compute occupied ranges or counts without enumerating every possible subnet.

Useful? React with 👍 / 👎.

Comment thread scripts/health.py Outdated
Comment on lines +365 to +367
inspected = run(["docker", "network", "inspect", name])
if inspected.returncode != 0:
return empty

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Tolerate networks removed during inspection

When a workflow finishes and removes its per-job network after docker network ls but before this separate inspect call, the normal concurrent deletion produces a nonzero result and the entire headroom check becomes unavailable, which evaluate reports as critical. On an active fleet this race can therefore generate false unhealthy reports during ordinary job teardown; handle a confirmed not-found by refreshing or skipping that vanished network while still failing closed for genuine inspection errors.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

Comment thread scripts/status_receiver.py
@Nickfost

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8ae88b83b9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/health.py Outdated
Comment on lines +271 to +274
"network": {
key: snapshot.get("docker_network_headroom", {}).get(key, 0)
for key in ("configured", "used", "free", "legacy")
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve compatibility with deployed v1 receivers

When status reporting is enabled but the independently deployed receiver has not yet been upgraded, every controller report now unconditionally includes docker.network, even when docker_network_policy is absent. The previous v1 receiver accepts exactly healthy and oom, so upgrading or rolling back these separately managed components in the wrong order makes all reports fail as invalid_report and leaves external monitoring stale. Fresh evidence in the final tree is that the sender emits the field without a configured policy while the receiver retains a separate upgrade/rollback lifecycle; omit the field until the policy is configured or document and test a receiver-first compatibility sequence.

AGENTS.md reference: AGENTS.md:L66-L66

Useful? React with 👍 / 👎.

"docker_network_policy": {
"reserve_subnets": 1,
"default_address_pools": [
{"base": "198.51.100.0/24", "size": 24 + args.max_runners.bit_length()},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep generated Docker subnets large enough for Compose jobs

For accepted values starting at --max-runners 32, this formula emits /30 subnets (/31 at 64 and /32 at 128). A /30 bridge has only two usable IPv4 addresses, one consumed by Docker's gateway, so a contract-compliant Compose job with a test container and a database cannot attach both services; the smaller prefixes are worse. Expand the base or reject maxima that cannot retain a practical subnet size rather than satisfying runner capacity by shrinking each job network.

AGENTS.md reference: AGENTS.md:L13-L21

Useful? React with 👍 / 👎.

Comment thread templates/config-repository/scripts/validate.py
Comment thread scripts/desired_state.py Outdated
Comment on lines +188 to +189
if configured < max_runners + reserve:
raise DesiredStateError(f"{path}: policy cannot satisfy max_runners plus reserve")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve a subnet for the controller's Compose network

When a policy provides exactly max_runners + reserve_subnets slots, this check approves it without accounting for the persistent ci-fleet_default network created by deploy/compose.yaml. Once that network is allocated from the configured pools, peak usage consumes max_runners + 1 slots, so only reserve_subnets - 1 remain; with the common reserve of one, full runner capacity is immediately reported as exhausted rather than retaining the reviewed reserve. Include the controller network in the required capacity calculation.

AGENTS.md reference: AGENTS.md:L44-L45

Useful? React with 👍 / 👎.

Comment thread templates/config-repository/scripts/validate.py
@Nickfost

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5247252b50

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread templates/config-repository/scripts/validate.py
Comment thread scripts/health.py
Comment thread scripts/status_receiver.py Outdated
Comment on lines +244 to +245
network = docker.get("network")
if network is not None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject null network summaries

When an authenticated report includes the optional key as "network": null, the Docker key-set check accepts it and this guard skips all network validation, so the receiver stores a report that violates the published v1 schema's object requirement. Consumers that treat the presence of network as a validated aggregate object can then fail or misclassify the controller; distinguish an absent key from a present null value and reject the latter as invalid_report.

Useful? React with 👍 / 👎.

validation.require(type(memory) is int and memory >= 512, f"{path}.runner_resources.memory_mib", "must be at least 512 MiB")
network_policy = controller.get("docker_network_policy")
capacity_maximum = maximum if state != "disabled" and type(maximum) is int and maximum > 0 else 0
if network_policy is not None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject an explicitly null Docker network policy

When a controller contains "docker_network_policy": null, exact_keys permits the optional key and this condition skips semantic validation, so both normal and strict validation accept a value that the updated JSON schema requires to be an object. The installer does not execute JSON Schema validation separately, meaning this malformed desired state is deployable and silently disables the promised network-capacity inspection; validate whenever the key is present rather than only when its value is non-null.

AGENTS.md reference: templates/config-repository/AGENTS.md:L29-L29

Useful? React with 👍 / 👎.

@Nickfost

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 80da5050b3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/health.py Outdated
Comment on lines +365 to +366
if name == "bridge":
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count an overlapping default bridge

When the daemon's built-in bridge is deliberately assigned a subnet inside a configured address pool (for example via bip), this unconditional skip omits the allocation even though Docker cannot reuse an overlapping pool slot. The report can therefore overstate free capacity and remain healthy at exhaustion; inspect bridge to count any overlap while exempting it only from the legacy-network warning.

AGENTS.md reference: AGENTS.md:L45-L45

Useful? React with 👍 / 👎.

Comment on lines +189 to +190
if type(size) is not int or size < 0 or size > 32:
validation.errors.append(f"{pool_path}.size: must be an IPv4 prefix length between 0 and 32")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject undersized Docker subnet allocations

For a hand-edited private policy, strict validation still accepts size values up to /32, despite the initializer enforcing /29 or larger for usable Compose networks. A /31 or /32 allocation cannot provide the gateway and container addresses required by ordinary jobs, so a policy can pass schema, semantic validation, and rendering while Docker network creation fails; enforce the practical prefix ceiling in the schema and both validators.

AGENTS.md reference: templates/config-repository/AGENTS.md:L29-L29

Useful? React with 👍 / 👎.

Comment on lines +214 to +217
configured = sum(1 << (item["size"] - item["network"].prefixlen) for item in parsed)
if configured < max_runners + reserve + 1:
validation.errors.append(
f"{path}: network capacity cannot satisfy max_runners + reserve_subnets + one controller Compose network"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Account for every per-job Compose network

For a compliant project whose Compose file defines two internal networks, this minimum assumes only one subnet per concurrent runner. For example, a two-runner controller with one reserved subnet passes with four slots, but the controller network plus both jobs' two networks require five, so the final network creation fails at otherwise valid maximum capacity. Either model a reviewed networks-per-runner bound or enforce a single-network project contract before claiming this capacity is sufficient.

AGENTS.md reference: AGENTS.md:L45-L45

Useful? React with 👍 / 👎.

Comment thread templates/config-repository/scripts/validate.py
@Nickfost

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9679136e1b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread templates/config-repository/scripts/validate.py
Comment thread scripts/health.py
@Nickfost

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: ce621fb68f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Nickfost
Nickfost marked this pull request as ready for review August 27, 2026 06:53
@Nickfost
Nickfost merged commit 46bdf47 into main Aug 27, 2026
1 check passed
@Nickfost
Nickfost deleted the feat/issue-81-network-policy branch August 27, 2026 06:53

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce621fb68f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +620 to +624
if "docker_network_policy" in new:
validation.require(
current_evidence.get("engine_ref") == new.get("engine_ref")
and current_evidence.get("docker_network_policy_config") is True,
f"$.controllers.{name}.docker_network_policy",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Run network evidence checks on every upgrade validation

When an installed controller retains this policy while selecting an incompatible engine, the evidence check runs only inside validate_transition, but the production validation calls in scripts/install-worker-controller.sh:191 and scripts/remote-reconcile.sh never provide --previous-config, so this block is skipped. The desired state therefore passes strict validation without matching docker_network_policy_config evidence and is rejected only later when rendering against the selected engine manifest, blocking reconciliation. Fresh evidence in the final tree is that the new retained-policy check remains reachable only through this optional transition path; invoke transition validation from the upgrade/reconcile paths or enforce current evidence during ordinary config validation.

AGENTS.md reference: AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Nickfost added a commit that referenced this pull request Aug 28, 2026
* feat: define Docker network capacity policy

* fix: address Docker network policy review findings

* fix: harden Docker network inspection

* test: use reserved network capacity range

* fix: harden Docker network policy validation

* fix: harden network policy rollout validation

* fix: enforce Docker network capacity policy

* fix: harden network policy rollout reporting

---------

Co-authored-by: Nick's Hermes <1572453+Nickfost@users.noreply.github.com>
Nickfost added a commit that referenced this pull request Aug 28, 2026
…/controller/routine-go-9b6dc1ae62

* origin/main:
  feat: define Docker network capacity policy (#82)
Nickfost added a commit that referenced this pull request Aug 28, 2026
…d config

templates/config-repository/scripts/init.py hardcoded an RFC 5737
documentation address (198.51.100.0/24) as the default Docker
default_address_pools base. PR #82's schema validation rejects RFC 5737
ranges under strict mode, so any config init.py generates now fails
'validate.py --strict'. The bootstrap test builds its fixture via init.py
and the bootstrap strict-validates that pinned config, so the mocked
bootstrap test could not bring up its routing callback.

Emit 10.64.0.0/24, the operational CIDR the schema's own test
(test_policy.py) uses as the valid example. The hand-written template
fleet.json examples remain RFC 5737 placeholders because CI validates
them non-strict (intended 'replace before use' examples).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant