Skip to content

Generate deterministic plans from seeded scenarios#8

Merged
berendt merged 7 commits into
mainfrom
implement/issue-3-deterministic-plan-generator
Jun 24, 2026
Merged

Generate deterministic plans from seeded scenarios#8
berendt merged 7 commits into
mainfrom
implement/issue-3-deterministic-plan-generator

Conversation

@berendt

@berendt berendt commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Closes #3

Summary

Builds the left half of the test pipeline: a YAML scenario format and a deterministic generator that expands a scenario plus a seed into a fully-enumerated plan — the expected-state source of truth listing every network, subnet, router interface, port, and security-group rule with its intended attributes and references. The reproducibility contract is the point: the same scenario and seed yield a byte-identical plan, stable across runs and Go versions. Ships the neutron generate command, dotted-key --set overrides, strict schema validation, and apply --dry-run that prints the plan summary while making zero API calls.

Change set (in commit order)

  1. 72192db Add internal/plan expected-state model with validation — The Plan is pure data with slice-only collections so JSON encoding is byte-stable for the same input. Validate resolves every cross-resource reference and enforces the Phase 1 invariant that a subnet attaches to at most one router. Summary renders deterministic per-type counts for the dry-run preview.
  2. 0fbde08 Add internal/scenario types, parsing, validation, overrides — Types the YAML scenario format from README §6 (counts, ratios, distributions, topology, seed). Parse uses strict unmarshalling so a mistyped key fails loudly; Validate emits field-named, actionable messages; Set applies dotted --set overrides with type-safe parsing. Promotes gopkg.in/yaml.v2 from an indirect to a direct dependency (already in go.sum, no new module).
  3. 7167bcb Add deterministic seeded plan generator with golden testsGenerate draws from math/rand v1 (frozen sequence) in a fixed order and emits every collection in a fixed order. IPs are allocated deterministically via net/netip from three non-overlapping ranges: explicit IPv4 /24 from 10.0.0.0/8, subnet pools as /16 from 172.16.0.0/12, and IPv6 /64 from fd00::/16. A committed golden plan (testdata/golden/small.plan.json) locks byte-stability; regenerate intentionally with -update.
  4. ee965ed Add neutron generate command writing plan JSONgenerate expands a scenario into a plan and writes indented JSON to --out or stdout; the confirmation line goes to the log so piped stdout stays clean. --scenario is required, --set applies overrides, and the global --seed overrides the scenario seed. buildPlanFromFlags (load → override → generate) is factored out for reuse by apply.
  5. 01db05e Implement apply --dry-run plan preview without API callsapply --dry-run expands the scenario and prints its summary with zero API calls. The command imports neither internal/config nor gophercloud, so the dry-run path structurally cannot reach the API; the non-dry-run executor returns a clear not-implemented error tracked to issue Apply plans against Neutron with tagging, concurrency, and metrics #4.
  6. 74bcd76 Document resolved IPv6/network-type decisions and status — Marks the IPv6 and network-types open questions resolved to match what the generator emits: dual-stack subnets under distribution.ipv6_ratio, and plain geneve/vxlan tenant networks with no provider attributes. Documents deterministic generation and CIDR allocation, and refreshes the status line.
  7. 966ba2c Sync neutron namespace doc comment with implemented commands — Updates the namespace doc comment that still described generate and apply as stubs.

Notes for the reviewer

  • Scope vs. issue: The issue scopes the left half of the pipeline (scenario → plan → JSON + --dry-run). The real apply executor against a live cloud is intentionally out of scope — it returns a not-implemented error tracked to issue Apply plans against Neutron with tagging, concurrency, and metrics #4. No divergence from the issue.
  • Resolved decisions: Per the issue's note that this work "resolves the IPv6 and network-types decisions insofar as they shape what the generator emits," dual-stack subnets are emitted under ipv6_ratio and networks are plain tenant networks with no provider attributes (74bcd76).
  • Testing: Golden-file test pins byte-stability of the generated plan; unit tests cover plan validation, scenario parsing/validation/overrides, and the generate / apply --dry-run commands.

Implemented by planwerk-review a24f4d9 with Claude:claude-opus-4-8

berendt added 7 commits June 24, 2026 10:00
The plan is the fully-expanded expected state produced from a scenario
plus a seed: every network, subnet, router interface, port, and security
group rule with its intended attributes and references. Model it as pure
data with slice-only collections so that JSON encoding is byte-stable for
the same input, a prerequisite for the reproducibility contract.

Validate resolves every cross-resource reference and enforces the Phase 1
invariant that a subnet attaches to at most one router. Summary renders a
deterministic per-type count for the upcoming apply --dry-run preview.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Christian Berendt <berendt@b42labs.com>
Define the YAML scenario format from README §6 (counts, ratios,
distributions, topology, seed) as typed structs. Parse decodes with
strict unmarshalling so a mistyped key fails loudly rather than being
silently dropped. Validate enforces the semantic constraints with
field-named, actionable messages, and Set applies the documented dotted
--set overrides with type-safe parsing.

This promotes gopkg.in/yaml.v2 from an indirect to a direct dependency;
it was already present in go.sum, so no new module is introduced.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Christian Berendt <berendt@b42labs.com>
Generate expands a scenario plus its seed into a fully-enumerated plan.
Reproducibility is the contract: it draws from math/rand v1 (whose
sequence is frozen for compatibility) in a fixed order and emits every
collection in a fixed order, so the same scenario and seed yield a
byte-identical plan. IP addresses are allocated deterministically from
three non-overlapping ranges via net/netip: explicit IPv4 /24 blocks
from 10.0.0.0/8, subnet pools as /16 from 172.16.0.0/12, and IPv6 /64
blocks from fd00::/16.

A committed golden plan locks byte-stability across runs and Go
versions; regenerate it intentionally with -update. The generator
resolves the IPv6 and network-type decisions: dual-stack subnets are
emitted under ipv6_ratio, and networks are plain tenant networks with no
provider attributes.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Christian Berendt <berendt@b42labs.com>
generate expands a scenario into a plan and writes it as indented JSON to
--out or stdout; the one-line confirmation goes to the log so piped
stdout stays clean. --scenario is required, --set applies dotted-key
overrides, and the global --seed overrides the scenario seed when passed.

buildPlanFromFlags (load, override, generate) is factored out so the
apply command can reuse it without API calls. The generate stub is
replaced with the real command.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Christian Berendt <berendt@b42labs.com>
apply --dry-run expands the scenario into a plan and prints its summary,
making zero API calls so an operator can validate a scenario before
touching a cloud. The command imports neither internal/config nor
gophercloud, so the dry-run path structurally cannot reach the API; the
non-dry-run executor returns a clear not-implemented error tracked to
issue #4.

The apply stub is replaced with the real command, and the stub
not-implemented test is narrowed to the remaining stubs.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Christian Berendt <berendt@b42labs.com>
Mark the IPv6 and network-types open questions resolved to match what the
generator now emits: dual-stack subnets under distribution.ipv6_ratio,
and plain geneve/vxlan tenant networks with no provider attributes. Note
in the scenario section that generation is deterministic and how plan
CIDRs are allocated, and refresh the status line to reflect the scenario
schema, generator, generate command, and apply --dry-run now existing.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Christian Berendt <berendt@b42labs.com>
generate and apply are no longer stubs, so the namespace doc comment that
described every Phase 1 subcommand as a stub is now inaccurate. Update it
to reflect what is implemented.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Christian Berendt <berendt@b42labs.com>
@berendt
berendt marked this pull request as ready for review June 24, 2026 08:33
@berendt
berendt merged commit 2774448 into main Jun 24, 2026
2 checks passed
@berendt
berendt deleted the implement/issue-3-deterministic-plan-generator branch June 24, 2026 08:35
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.

Generate deterministic plans from seeded scenarios

1 participant