refactor(operator): route render inputs through ClusterIntent seam - #461
WentingWu666666 wants to merge 9 commits into
Conversation
5bae872 to
e765518
Compare
…hase 0a)
Extend the product-neutral ClusterIntent so GetCnpgClusterSpecFromIntent
renders the Tier A/B builder inputs from the intent instead of reading
*DocumentDB directly. No runtime behavior change: the rendered CNPG
Cluster is byte-identical.
- product: add Resource, TLS{GatewaySecretName, PostgresCertificates},
Monitoring{Enabled}, LogLevel, MaxStopDelay, Postgres.Parameters, and
FeatureGates.ChangeStreams to ClusterIntent; populate them in
DocumentDBAdapter.ToClusterIntent (defaults preserved). Add exported
ResourceFromSpec.
- cnpg: neutralize MergeParameters and ComputeResourceSplit via
MergeParametersResolved / ComputeResourceSplitFromResource, keeping the
*DocumentDB functions as thin wrappers (avoids the product<->cnpg import
cycle and keeps existing tests green). Renderer now reads params,
resource split, log level, stop delay, Postgres certs, and gateway TLS
secret from the intent; remove dead helpers.
- Monitoring/OTel inputs are intentionally left on *DocumentDB for a
follow-up (Phase 0b).
- Add cnpg_intent_drift_test.go: a drift guard asserting the intent render
path matches the retained *DocumentDB computations across a spec matrix.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 03549680-3e40-4bea-8144-f0d0cfc6cb46
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
e765518 to
e52214c
Compare
|
🤖 Auto-triaged by documentdb-triage-tool. Applied: Reasoningcomponent from path globs (controllers, test); effort from diff stats (364+63 LOC, 8 files); LLM: Multi-file internal refactor routing CNPG cluster rendering through a product-neutral ClusterIntent seam, touching multiple packages (internal/product, internal/cnpg) with new tests and no API/schema changes, as groundwork for a future shared operator. If a label is wrong, remove it manually and ping |
…e 0b) Neutralize the monitoring render path so the CNPG builder no longer reads the DocumentDB CRD. internal/otel is now product-agnostic: its config generation takes a neutral otel.MonitoringConfig instead of *dbpreview.MonitoringSpec. product.MonitoringConfigFromSpec is the single CRD->neutral mapper shared by the adapter and the controller's ConfigMap reconcile. GetCnpgClusterSpecFromIntent drops its *DocumentDB parameter; the renderer computes the OTel config-map name, Prometheus port, and the change-detection config hash on the fly from intent.Monitoring + req.Namespace, preserving the original design. The drift guard gains monitoring cases asserting otelConfigMapName/prometheusPort/otelConfigHash and the monitor role match the direct otel computation, proving the render stays byte-identical. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 03549680-3e40-4bea-8144-f0d0cfc6cb46 Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Make GetCnpgClusterSpecFromIntent fully self-contained by moving the remaining loose render inputs onto the neutral ClusterIntent: - Identity gains Namespace; ObjectMeta and the OTel config namespace now source name/namespace from intent.Identity instead of ctrl.Request. - Storage gains StorageClass (a reconcile/replication-context runtime input). - ClusterIntent gains IsPrimaryRegion (gates recovery bootstrap). The renderer signature drops req, serviceAccountName (already dead), storageClass, and isPrimaryRegion in favor of (intent, log). The DocumentDB adapter now sets Identity.Namespace; the GetCnpgClusterSpec wrapper and the controller populate StorageClass/IsPrimaryRegion before calling the seam. This is zero-behavior-change in production (req coordinates always equal the CR's own name/namespace); the drift guard proves the rendered Cluster is byte-identical, including the OTel config hash. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 03549680-3e40-4bea-8144-f0d0cfc6cb46 Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Restructure the product-neutral ClusterIntent to follow the DocumentDB CRD
grouping and vocabulary where the mapping is a direct passthrough:
- MaxStopDelay -> Timeouts.StopDelay (new Timeouts struct)
- SidecarInjectorPlugin/WALReplicaPlugin -> Plugins.SidecarInjectorName/
WalReplicaName (new Plugins struct)
Move reconcile-time inputs off the intent into a narrow neutral
RenderContext{StorageClass, IsPrimaryRegion}, since these are not part of
any product's desired state. GetCnpgClusterSpecFromIntent now takes
(intent, rctx, log).
The adapter now honors db.Spec.Plugins.WalReplicaName as the single source
of truth for the WAL replica plugin (falling back to the profile default);
this is byte-identical today since the WAL replica wiring is a disabled TODO.
Zero behavior change: TestRenderIntentSeamNoDrift stays green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 03549680-3e40-4bea-8144-f0d0cfc6cb46
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
…erContext Drop the RenderContext struct and pass the two reconcile-time inputs directly to GetCnpgClusterSpecFromIntent(intent, storageClass, isPrimaryRegion, log). Zero behavior change: TestRenderIntentSeamNoDrift stays green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 03549680-3e40-4bea-8144-f0d0cfc6cb46 Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
- restore the orphaned ComponentResource doc comment - drop a redundant wrapper comment already covered by the function doc - remove a pointless local alias when taking storageClass's address No behavior change; TestRenderIntentSeamNoDrift stays green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 03549680-3e40-4bea-8144-f0d0cfc6cb46 Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Go does not require unused function parameters to be discarded (only unused locals). Remove `_ = req` / `_ = serviceAccountName` and fold the rationale into the function doc comment. No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 03549680-3e40-4bea-8144-f0d0cfc6cb46 Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
…Node Mirror the DocumentDB CR's topology vocabulary on the intent: carry NodeCount and rename the per-node instance count to InstancesPerNode. NodeCount is carried for completeness but not yet rendered (no active reader today), so the CNPG output is unchanged. No behavior change; TestRenderIntentSeamNoDrift stays green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 03549680-3e40-4bea-8144-f0d0cfc6cb46 Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
There was a problem hiding this comment.
🟡 Changes recommended
The compatibility wrapper can produce incorrect object identity, and the advertised ChangeStreams intent field is missing; scope and drift-test alignment also remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR routes additional CNPG rendering inputs through ClusterIntent while adding neutral resource, PostgreSQL, and monitoring seams.
Changes:
- Expands intent and adapter mappings.
- Refactors CNPG and OTel helpers around neutral inputs.
- Updates rendering and adds related test coverage.
File summaries
| File | Reviewed changes |
|---|---|
operator/src/internal/product/intent.go |
Extends ClusterIntent models. |
operator/src/internal/product/intent_test.go |
Tests intent mappings. |
operator/src/internal/product/documentdb.go |
Maps DocumentDB configuration into intent. |
operator/src/internal/otel/config.go |
Adds neutral monitoring configuration. |
operator/src/internal/otel/config_test.go |
Tests monitoring configuration. |
operator/src/internal/controller/documentdb_controller.go |
Routes rendering and configuration through intent. |
operator/src/internal/cnpg/resource_split.go |
Adds neutral resource splitting. |
operator/src/internal/cnpg/pg_defaults.go |
Adds neutral parameter merging. |
operator/src/internal/cnpg/pg_defaults_test.go |
Tests parameter merging. |
operator/src/internal/cnpg/cnpg_intent_test.go |
Tests intent-based rendering. |
operator/src/internal/cnpg/cnpg_intent_drift_test.go |
Adds render drift coverage. |
operator/src/internal/cnpg/cnpg_cluster.go |
Routes cluster rendering through intent. |
operator/src/internal/cnpg/cnpg_cluster_test.go |
Updates cluster renderer tests. |
Review details
Suppressed comments (3)
operator/src/internal/cnpg/cnpg_intent_drift_test.go:146
- This drift test does not compare the intent path with an independent pre-refactor path:
GetCnpgClusterSpecalready delegates toGetCnpgClusterSpecFromIntent, and the expected parameter/resource helpers also delegate through the new adapter/neutral implementations. It can therefore pass even if the intent mapping or renderer changes, and it does not establish byte-identical fullClusteroutput. Keep an independent baseline or compare the complete rendered object against a legacy/fixture result.
spec := GetCnpgClusterSpec(req, db, "", "test-sa", "", true, log).Spec
operator/src/internal/controller/documentdb_controller.go:1035
- The PR description says Phase 0b intentionally leaves Monitoring/OTel inputs reading
*DocumentDB, but this change maps the monitoring spec to a neutral type and routes ConfigMap generation through it here. Please either keep this out of the Phase 0a implementation or update the description and acceptance/testing scope so the phase boundary is explicit.
configData, err := otelcfg.GenerateConfigMapData(documentdb.Name, namespace, product.MonitoringConfigFromSpec(documentdb.Spec.Monitoring))
operator/src/internal/product/intent.go:79
- The PR description says
ClusterIntent.FeatureGatesis extended withChangeStreams, but this definition omits that field and instead folds the gate intoPostgres.Parameters["wal_level"]. That may be the intended product-neutral design, but it contradicts the advertised seam and prevents intent consumers from inspecting the gate; please align the implementation and description before merging.
// FeatureGates carries the resolved, product-neutral feature-gate flags the
// builder acts on. Only genuinely cross-product (infrastructure) gates belong
// here; product-specific gates are expressed through their concrete effect (for
// example DocumentDB change streams contributing Postgres.Parameters
// wal_level=logical) instead of leaking into this struct.
type FeatureGates struct {
// IOUring relaxes the postgres seccomp profile and enables io_method=io_uring.
// It is an infrastructure concern shared across products.
IOUring bool
- Files reviewed: 13/13 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| intent.Images.PostgresExtension = documentdbImage | ||
| } | ||
| return GetCnpgClusterSpecFromIntent(req, documentdb, intent, serviceAccountName, storageClass, isPrimaryRegion, log) | ||
| return GetCnpgClusterSpecFromIntent(intent, storageClass, isPrimaryRegion, log) |
There was a problem hiding this comment.
Thanks — this isn't a production issue, and I'd like to keep the current behavior. Rationale:
GetCnpgClusterSpec(req, …)is now a test-only compatibility shim. The reconciler callsGetCnpgClusterSpecFromIntentdirectly, and it builds identity from the fetched CR (documentdb.ObjectMeta) viaintent.Identity. InReconcile,documentdbis loaded withr.Get(ctx, req.NamespacedName, …), so its coordinates always equalreq— production identity (and the OTel namespace hash) is correct.- Sourcing identity from the CR itself is deliberate and more correct than
req: the CNPGClusteris owned by that CR, so its name/namespace/owner-refs/labels must track the CR. The pre-refactor wrapper was actually inconsistent here (ObjectMeta fromreq, but OTel/labels/owner-refs fromdb.Name); this unifies on the CR. - Copying
reqintoIdentitywould also breakTestRenderIntentSeamNoDrift: its cases carry noObjectMeta, and the expected OTel config hash is computed fromdb.Namespace, so forcingreq.Namespacewould diverge.
The only real smell is that req/serviceAccountName are now vestigial on this shim (documented as accepted-for-compatibility). Happy to drop them and migrate the ~40 test call-sites in a follow-up if preferred.
| Postgres: pg, | ||
| Resource: ResourceFromSpec(db.Spec.Resource), | ||
| TLS: tls, | ||
| Monitoring: MonitoringConfigFromSpec(db.Spec.Monitoring), |
There was a problem hiding this comment.
Resolved by aligning the PR description with the implementation. The "Phase 0b deferred" wording was stale from an earlier revision; the description has been updated to state that monitoring/OTel inputs are routed through the intent (Monitoring is a resolved otel.MonitoringConfig, and the renderer consumes config-map name/hash + Prometheus port from it). Mapping the full exporter config into ClusterIntent here is intentional and remains byte-identical — TestRenderIntentSeamNoDrift cross-checks the OTel config hash against a direct otelcfg.GenerateConfigMapData(db.Name, db.Namespace, mon) computation.
Re: Copilot review — addressing the suppressed commentsThanks for the review. Summarizing how the three suppressed comments are addressed, since they don't have inline threads: 1. 2. Monitoring routed through the intent (controller.go). Same root cause — the "Phase 0b deferred" note was stale. Monitoring/OTel now flows through the intent by design; the description reflects this. 3. Drift test independence (cnpg_intent_drift_test.go). Fair point that the parameters and resource-split comparisons delegate through the same neutral helpers on both sides. However, byte-identical-ness is established by the suite as a whole, not the drift test alone:
So the drift test is a supplementary consistency guard on top of a literal-baseline suite. The only fully-independent alternative for the circular parts would be golden fixtures captured from |
| // Parameters: intent path must equal the direct MergeParameters over the | ||
| // same memory-aware split. | ||
| wantMem := ComputeResourceSplit(db, DefaultSplitConfig()).PostgresMemoryBytes | ||
| wantParams := MergeParameters(db, wantMem) |
There was a problem hiding this comment.
This guard can't detect drift introduced by this PR — both sides run the same code.
MergeParameters (pg_defaults.go:115-118) and ComputeResourceSplit (resource_split.go:106-109) were rewritten in this PR to delegate through ToClusterIntent, which is exactly what the renderer calls. So wantParams and the rendered parameters are the same expression.
I verified it: commenting out Parameters: db.Spec.Postgres.Parameters in ToClusterIntent — silently dropping every user-supplied GUC from the rendered cluster — leaves TestRenderIntentSeamNoDrift green. The existing pg_defaults_test.go catches that regression; this test does not. Same structure for the resource-split assertions at L150/L191.
The logLevel / maxStopDelay / certificates / gatewayTLSSecret assertions are independent and worth keeping. For the parameter and split cases, either pin concrete expected values (e.g. assert wal_level=logical literally in the change-streams case), or drop the *DocumentDB wrappers so the test is forced to state its expectations directly.
This is the PR's central correctness claim, so I'd want it resolved before merge. It also matches Copilot's suppressed comment on this line.
| // product-neutral OTel collector config carried on the intent. It is the single | ||
| // CRD->neutral mapping shared by the builder and the controller's ConfigMap | ||
| // reconciliation, so generated collector config (and its hash) stays consistent. | ||
| func MonitoringConfigFromSpec(spec *dbpreview.MonitoringSpec) otelcfg.MonitoringConfig { |
There was a problem hiding this comment.
This mapping has no test anywhere.
The otel/config_test.go cases that used to cover CRD → collector config were converted to build MonitoringConfig{...} literals, so the downstream half kept its coverage and the CRD half lost all of it. Now untested: spec == nil, Exporter == nil, Exporter.Prometheus != nil → Prometheus = true (the pointer-presence → bool conversion, the easiest line here to get wrong), OTLP.Endpoint == "", and the Enabled passthrough.
The drift test uses this same function as its own oracle, so it won't catch a mismapping either. A small table test in internal/product covering those five cases would close it.
| // accepted for call-site compatibility but no longer consumed: object | ||
| // coordinates now come from the intent's Identity, and the service account is | ||
| // not read by the renderer. | ||
| func GetCnpgClusterSpec(req ctrl.Request, documentdb *dbpreview.DocumentDB, documentdbImage, serviceAccountName, storageClass string, isPrimaryRegion bool, log logr.Logger) *cnpgv1.Cluster { |
There was a problem hiding this comment.
GetCnpgClusterSpec, MergeParameters, ComputeResourceSplit and ProtectedParameters all have zero non-test callers at head — the controller now calls GetCnpgClusterSpecFromIntent directly. The doc comment says "retained for call-site compatibility," but there are no production call sites left, and it concedes two of the six parameters are ignored (serviceAccountName was already dead before this PR).
Worth deleting here rather than deferring: it also resolves the drift-test issue above, since the tests would then have to state their expectations directly instead of calling back through the new path.
Same vein, both unread at head: Topology.NodeCount (no reader anywhere, as the commit message notes — it's also what motivated the Instances → InstancesPerNode rename), and the new Plugins.WalReplicaName override in product/documentdb.go:94-97 (the only other resolution site, physical_replication.go:90-102, is inside a commented-out TODO re-enable block).
Address reviewer feedback requesting reduced ceremony without changing
rendered output (verified byte-identical to the pre-cleanup commit via the
new golden fixtures):
- Replace the 243-line field-by-field drift test with a compact full-object
golden test (cnpg_render_golden_test.go) that renders the whole CNPG
Cluster for the same spec matrix and diffs it against testdata/render
fixtures. Shorter test code, stronger regression guard.
- Remove the test-only *DocumentDB wrappers ComputeResourceSplit and
MergeParameters, leaving a single neutral API (ComputeResourceSplitFromResource
/ MergeParametersResolved). Test call sites use small local helpers.
- Drop process-narration comments ("first product adapter", "progressively
rewired onto the seam", "retained for direct callers"); keep invariant docs.
go.mod: promote go-cmp and sigs.k8s.io/yaml from indirect to direct (used by
the golden test); no new dependencies.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 03549680-3e40-4bea-8144-f0d0cfc6cb46
Signed-off-by: Wenting Wu <wentingwu@microsoft.com>
Summary
Routes the CNPG cluster render path through the product-neutral
ClusterIntentseam introduced in #446.GetCnpgClusterSpecFromIntent(intent, storageClass, isPrimaryRegion, log)now renders the CNPGClusterfrom a resolved, product-neutral model instead of reading*DocumentDBdirectly. No runtime behavior change — the rendered CNPGClusteris byte-identical, guarded by a drift test.What changed
internal/product—ClusterIntentis now a self-contained, resolved desired-state model. Groups mirror the DocumentDB CRD where the mapping is a direct passthrough:Images,Topology{NodeCount, InstancesPerNode, Affinity},Storage{PvcSize},Identity,Postgres,Resource,TLS{GatewaySecretName, PostgresCertificates},Monitoring(otel.MonitoringConfig),LogLevel,Timeouts{StopDelay},FeatureGates{IOUring},Bootstrap,CredentialSecret,Plugins{SidecarInjectorName, WalReplicaName},Product.DocumentDBAdapter.ToClusterIntentresolves images, credential secret, and plugin names (honoring the spec, else profile defaults), and contributes DocumentDB change-streams as aPostgres.Parametersentry (wal_level=logical) rather than leaking a product-specific feature gate into the neutral model.internal/cnpg— the renderer reads every input from the intent. NeutralizedMergeParameters/ComputeResourceSplitviaMergeParametersResolved/ComputeResourceSplitFromResourceto avoid aproduct↔cnpgimport cycle (the*DocumentDBfunctions remain thin wrappers). Params, resource split, log level, stop delay, Postgres certs, gateway TLS secret, and OTel/monitoring config all flow through the intent.storageClass,isPrimaryRegion) are passed as render params, not carried on the intent, keeping the intent a pure function of the CR.Not in scope (owned by other reconcile paths)
ClusterReplication,ExposeViaService,Backup, andSchemaVersionare not render inputs and remain owned by their existing controllers.NodeCountis carried on the intent for completeness but is not yet rendered (no active reader today).Testing
cnpg_intent_drift_test.go: byte-identical guard asserting the intent render path matches the retained*DocumentDBcomputations across a spec matrix (TLS spec+status, user params, resource envelope/overrides, IOUring, change-streams, custom logLevel/stopDelay, process identity, monitoring).make test(envtest) green across all packages;go build/go vet/gofmtclean. No CRD/API/Helm changes; no generated-file drift.