Generate deterministic plans from seeded scenarios#8
Merged
Conversation
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
marked this pull request as ready for review
June 24, 2026 08:33
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
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 generatecommand, dotted-key--setoverrides, strict schema validation, andapply --dry-runthat prints the plan summary while making zero API calls.Change set (in commit order)
72192dbAdd internal/plan expected-state model with validation — ThePlanis pure data with slice-only collections so JSON encoding is byte-stable for the same input.Validateresolves every cross-resource reference and enforces the Phase 1 invariant that a subnet attaches to at most one router.Summaryrenders deterministic per-type counts for the dry-run preview.0fbde08Add internal/scenario types, parsing, validation, overrides — Types the YAML scenario format from README §6 (counts, ratios, distributions, topology, seed).Parseuses strict unmarshalling so a mistyped key fails loudly;Validateemits field-named, actionable messages;Setapplies dotted--setoverrides with type-safe parsing. Promotesgopkg.in/yaml.v2from an indirect to a direct dependency (already ingo.sum, no new module).7167bcbAdd deterministic seeded plan generator with golden tests —Generatedraws frommath/randv1 (frozen sequence) in a fixed order and emits every collection in a fixed order. IPs are allocated deterministically vianet/netipfrom three non-overlapping ranges: explicit IPv4/24from10.0.0.0/8, subnet pools as/16from172.16.0.0/12, and IPv6/64fromfd00::/16. A committed golden plan (testdata/golden/small.plan.json) locks byte-stability; regenerate intentionally with-update.ee965edAdd neutron generate command writing plan JSON —generateexpands a scenario into a plan and writes indented JSON to--outor stdout; the confirmation line goes to the log so piped stdout stays clean.--scenariois required,--setapplies overrides, and the global--seedoverrides the scenario seed.buildPlanFromFlags(load → override → generate) is factored out for reuse byapply.01db05eImplement apply --dry-run plan preview without API calls —apply --dry-runexpands the scenario and prints its summary with zero API calls. The command imports neitherinternal/confignorgophercloud, 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.74bcd76Document 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 underdistribution.ipv6_ratio, and plain geneve/vxlan tenant networks with no provider attributes. Documents deterministic generation and CIDR allocation, and refreshes the status line.966ba2cSync neutron namespace doc comment with implemented commands — Updates the namespace doc comment that still describedgenerateandapplyas stubs.Notes for the reviewer
--dry-run). The realapplyexecutor 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.ipv6_ratioand networks are plain tenant networks with no provider attributes (74bcd76).generate/apply --dry-runcommands.Implemented by planwerk-review a24f4d9 with Claude:claude-opus-4-8