Skip to content
This repository was archived by the owner on Jun 25, 2026. It is now read-only.

feat: add entityType validation for ContentConfiguration#702

Open
akafazov wants to merge 9 commits into
mainfrom
feat/entityType
Open

feat: add entityType validation for ContentConfiguration#702
akafazov wants to merge 9 commits into
mainfrom
feat/entityType

Conversation

@akafazov

@akafazov akafazov commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Validate entityType references in ContentConfigurations against known entity types defined via defineEntity segments across all CCs in the system. Unknown entity types are reported as hard validation failures.

The feature is gated behind --entity-type-validation-enabled (default false) on both the operator and server.

refers to platform-mesh/platform-mesh#75

@akafazov akafazov force-pushed the feat/entityType branch 2 times, most recently from f4f4424 to 1bf90e8 Compare March 13, 2026 11:58
@akafazov akafazov requested review from a team as code owners April 2, 2026 14:06
@OlegErshov

Copy link
Copy Markdown

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 2, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented Apr 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds entity-type validation: new EntityTypeRegistry with owner-aware reference counting; a feature flag (ServerConfig/OperatorConfig.EntityTypeValidationEnabled); server-side registry initialization from cluster ContentConfiguration CRs and periodic refresh; router and HttpValidateHandler wiring to invoke ValidateEntityTypes when a registry is present; ContentConfigurationSubroutine changes to lazily init and update the registry, plus Finalize support; extended validation interface and regenerated mocks; and comprehensive tests for registry, validation, subroutine, and endpoint behaviors.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🧹 Nitpick comments (2)
internal/server/validate_endpoint_test.go (1)

164-164: Remove commented-out code.

These commented-out handler instantiations are dead code and should be removed to maintain code cleanliness.

🧹 Proposed fix
-	// handler := NewHttpValidateHandler(log, validation.NewContentConfiguration(), nil)

Also applies to: 227-227, 300-300

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/validate_endpoint_test.go` at line 164, Remove the dead
commented-out handler instantiations (e.g., "// handler :=
NewHttpValidateHandler(log, validation.NewContentConfiguration(), nil)") from
the test file; locate occurrences of the NewHttpValidateHandler commented lines
(notably the three instances around the earlier review: the one shown plus the
ones at the other two comment locations) and delete those commented lines so the
test file contains only active code and no leftover commented handler setup.
internal/controller/controllerruntime/contentconfiguration_controller_cr.go (1)

49-53: Minor inconsistency with multiclusterruntime controller.

In multiclusterruntime/contentconfiguration_controller.go, both reader and registry are conditionally set to nil when validation is disabled. Here, cl is always passed. This is functionally equivalent (since the subroutine only uses the reader when registry is non-nil), but the approaches differ slightly.

Consider aligning the pattern for consistency:

♻️ Alternative approach for consistency
 func NewContentConfigurationReconcilerCR(log *logger.Logger, mgr ctrl.Manager, cfg config.OperatorConfig) *ContentConfigurationReconcilerCR {
-	cl := mgr.GetClient()
 	var subs []subroutine.Subroutine
 	if cfg.SubroutinesContentConfigurationEnabled {
 		var registry *validation.EntityTypeRegistry
+		var cl client.Client
 		if cfg.EntityTypeValidationEnabled {
 			registry = validation.NewEntityTypeRegistry()
+			cl = mgr.GetClient()
 		}
-		subs = append(subs, subroutines.NewContentConfigurationSubroutine(validation.NewContentConfiguration(), http.DefaultClient, cl, registry))
+		subs = append(subs, subroutines.NewContentConfigurationSubroutine(validation.NewContentConfiguration(), http.DefaultClient, cl, registry))
 	}
 	return &ContentConfigurationReconcilerCR{lifecycle: builder.
 		NewBuilder(controller.OperatorName, contentConfigurationReconcilerName, subs, log).
 		WithSpreadingReconciles().
 		WithConditionManagement().
-		BuildControllerRuntime(cl)}
+		BuildControllerRuntime(mgr.GetClient())}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/controller/controllerruntime/contentconfiguration_controller_cr.go`
around lines 49 - 53, The code creates registry only when
cfg.EntityTypeValidationEnabled is true but always passes cl into
NewContentConfigurationSubroutine, causing a pattern mismatch with
multiclusterruntime's controller; change the setup so both the reader/client
(cl) and registry are nil when validation is disabled (i.e., conditionally set
cl = nil when cfg.EntityTypeValidationEnabled is false) and then pass those
variables into NewContentConfigurationSubroutine (referencing registry, cl,
cfg.EntityTypeValidationEnabled, and NewContentConfigurationSubroutine) so the
controller matches the multiclusterruntime pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/server.go`:
- Around line 77-84: The startup code currently bulk-loads a snapshot via
initServerEntityTypeRegistry and hands that static
*validation.EntityTypeRegistry to server.CreateRouter, causing /validate to use
stale types; replace this with a live-updating registry or on-demand resolver:
implement a watch/informer (or a registry wrapper) that subscribes to
ContentConfiguration create/update/delete events and updates the in-memory
registry (or resolves the current set on each validation call), ensure
initServerEntityTypeRegistry becomes the bootstrap seed for the watcher and add
retry/health behavior on bootstrap failures so the handler never permanently
holds an incomplete registry; update cmd/server.go to pass this live
registry/resolver to CreateRouter and ensure validation.ContentConfiguration
consumers use the live view.

In `@pkg/subroutines/contentconfiguration.go`:
- Around line 114-127: The validator is currently run against r.entityRegistry
while it still contains this resource's prior definitions, causing stale types
and refcount inflation; before calling r.validator.ValidateEntityTypes with
validatedConfig, remove this resource's previous definitions from
r.entityRegistry, then load the candidate definitions (validate against registry
- previous + candidate), and only after successful validation apply the
remove/load transition to actually replace the registry entries (mirror this
same remove logic in the reconcile delete path so old definitions are cleaned up
on deletion); refer to r.entityRegistry, r.validator.ValidateEntityTypes,
validatedConfig, and the code that adds definitions (around the block that sets
instance.Status.Conditions) and apply the same fix for the other add/validate
block referenced (lines ~157-163).
- Around line 81-89: The registry bootstrap currently sets r.registryInitDone
true even when initEntityTypeRegistry fails because the Store(true) is executed
after the error check path returns; instead ensure registryInitDone.Store(true)
is only called after a successful initEntityTypeRegistry call and that the mutex
r.registryInitMu is unlocked in all code paths; propagate the init error (return
it) so the reconcile can retry rather than marking initialization complete.
Apply the same fix to the other bootstrap block referenced around lines 174-176
(use r.initEntityTypeRegistry, r.registryInitMu, and r.registryInitDone
consistently: lock, call initEntityTypeRegistry, on error unlock and return the
error without setting registryInitDone, on success set
registryInitDone.Store(true) then unlock).

In `@pkg/validation/entitytype.go`:
- Around line 57-74: Validate currently only checks r.types so entity types
defined in the same ContentConfiguration are treated as unknown; fix by merging
collectDefinedEntityTypes(cc) into the per-call lookup before checking
references: inside EntityTypeRegistry.Validate copy existing r.types into a
local map, add entries from collectDefinedEntityTypes(cc) (after
normalizeEntityType) into that local map, then use that combined map when
iterating refs from collectReferencedEntityTypes(cc); keep the existing RLock
for reading r.types and add a regression test ensuring a CC that defines and
references the same type (e.g., "project") passes validation.

---

Nitpick comments:
In `@internal/controller/controllerruntime/contentconfiguration_controller_cr.go`:
- Around line 49-53: The code creates registry only when
cfg.EntityTypeValidationEnabled is true but always passes cl into
NewContentConfigurationSubroutine, causing a pattern mismatch with
multiclusterruntime's controller; change the setup so both the reader/client
(cl) and registry are nil when validation is disabled (i.e., conditionally set
cl = nil when cfg.EntityTypeValidationEnabled is false) and then pass those
variables into NewContentConfigurationSubroutine (referencing registry, cl,
cfg.EntityTypeValidationEnabled, and NewContentConfigurationSubroutine) so the
controller matches the multiclusterruntime pattern.

In `@internal/server/validate_endpoint_test.go`:
- Line 164: Remove the dead commented-out handler instantiations (e.g., "//
handler := NewHttpValidateHandler(log, validation.NewContentConfiguration(),
nil)") from the test file; locate occurrences of the NewHttpValidateHandler
commented lines (notably the three instances around the earlier review: the one
shown plus the ones at the other two comment locations) and delete those
commented lines so the test file contains only active code and no leftover
commented handler setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: platform-mesh/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8aaf2f85-d3df-4b57-bb07-2919c0d5fa9c

📥 Commits

Reviewing files that changed from the base of the PR and between 48d2925 and 0e5ebe4.

📒 Files selected for processing (17)
  • Taskfile.yml
  • cmd/server.go
  • internal/config/config.go
  • internal/controller/controllerruntime/contentconfiguration_controller_cr.go
  • internal/controller/multiclusterruntime/contentconfiguration_controller.go
  • internal/server/router.go
  • internal/server/router_test.go
  • internal/server/validate_endpoint.go
  • internal/server/validate_endpoint_test.go
  • pkg/subroutines/contentconfiguration.go
  • pkg/subroutines/contentconfiguration_test.go
  • pkg/validation/entitytype.go
  • pkg/validation/entitytype_test.go
  • pkg/validation/interface.go
  • pkg/validation/mocks/mock_ExtensionConfiguration.go
  • pkg/validation/validate.go
  • pkg/validation/validate_test.go

Comment thread cmd/server.go
Comment thread pkg/subroutines/contentconfiguration.go
Comment thread pkg/subroutines/contentconfiguration.go Outdated
Comment thread pkg/validation/entitytype.go
akafazov added 5 commits May 14, 2026 11:10
Validate entityType references in ContentConfigurations against known
entity types defined via defineEntity segments across all CCs in the
system. Unknown entity types are reported as hard validation failures.

The feature is gated behind --entity-type-validation-enabled (default
false) on both the operator and server.

On-behalf-of: @SAP angel.kafazov@sap.com
Signed-off-by: Angel Kafazov <akafazov@cst-bg.net>
On-behalf-of: @SAP angel.kafazov@sap.com
Signed-off-by: Angel Kafazov <akafazov@cst-bg.net>
On-behalf-of: @SAP angel.kafazov@sap.com
Signed-off-by: Angel Kafazov <akafazov@cst-bg.net>
On-behalf-of: @SAP angel.kafazov@sap.com
Signed-off-by: Angel Kafazov <akafazov@cst-bg.net>
On-behalf-of: @SAP angel.kafazov@sap.com
Signed-off-by: Angel Kafazov <akafazov@cst-bg.net>
@akafazov

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

♻️ Duplicate comments (2)
pkg/subroutines/contentconfiguration.go (2)

105-119: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Remove the resource's previous definitions before validating and loading the new configuration.

Lines 107-108 validate against a registry that still contains this ContentConfiguration's previous definitions. Lines 151-154 only add the new definitions via Load, without removing the old ones. This causes several problems:

  • Stale types remain valid: Renaming or removing a defineEntity leaves the old type in the registry, and validation incorrectly accepts references to it.
  • Refcount inflation: Repeated reconciles keep calling Load for the same types, inflating reference counts.
  • No cleanup on deletion: When a ContentConfiguration is deleted, its definitions remain in the registry.

Before validating at line 107, remove this resource's previous definitions from the registry. After validation succeeds, apply a remove-then-load transition to replace the old definitions with the new ones. Additionally, ensure that ContentConfiguration deletion triggers removal of its definitions from the registry.

Also applies to: 151-154

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/subroutines/contentconfiguration.go` around lines 105 - 119, Remove this
ContentConfiguration's prior definitions from r.entityRegistry before calling
r.validator.ValidateEntityTypes on validatedConfig, so validation runs against
only the new definitions; implement a registry call like
r.entityRegistry.RemoveDefinitionsFor(instance.GetUID() or instance.Name)
immediately before the ValidateEntityTypes call. After validation succeeds,
replace the old definitions by performing a remove-then-load transition (call
the same RemoveDefinitionsFor(...) before invoking r.entityRegistry.Load(...) so
refcounts don't accumulate and stale types are cleared). Finally, ensure the
ContentConfiguration deletion path/finalizer calls the same removal logic to
unregister this resource's definitions from r.entityRegistry when the instance
is deleted.

165-167: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Propagate the List error to enable retry.

Returning nil on line 167 means the caller at line 76 sees no error, and line 80 marks registryInitDone as true even though the bulk load never happened. A transient API error would leave preexisting entity types missing from the registry until those resources happen to reconcile again.

🔄 Proposed fix
 	if err := r.k8sReader.List(ctx, &ccList); err != nil {
-		log.Warn().Err(err).Msg("failed to list ContentConfigurations for entity type registry initialization, registry will be populated incrementally")
-		return nil
+		return fmt.Errorf("list ContentConfigurations for entity type registry initialization: %w", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/subroutines/contentconfiguration.go` around lines 165 - 167, The List
error is currently swallowed (returning nil) in the function that calls
r.k8sReader.List, causing callers to treat initialization as successful; change
the function that contains the r.k8sReader.List call to return the encountered
error instead of nil (propagate the error up) so callers (e.g., the registry
initialization caller that sets registryInitDone) can retry or handle failure;
locate the List call and the enclosing function (the method on the receiver r in
contentconfiguration.go) and replace the nil return with return err, ensuring
the function signature supports returning an error or adjust the signature to do
so.
🧹 Nitpick comments (1)
internal/server/router_test.go (1)

127-128: ⚡ Quick win

Assert empty validation errors for successful registry cases.

The “valid” cases only assert status code. Decode the response there too and assert ValidationErrors is empty, so a 200 response with hidden validation failures cannot pass.

Also applies to: 144-145, 163-168

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/server/router_test.go` around lines 127 - 128, The tests that
currently only assert expectCode == http.StatusOK should also decode the JSON
response into the response struct used in this file (the register response that
contains ValidationErrors) and assert that ValidationErrors is empty; in other
words, after the existing status-code check, call the same decoder used
elsewhere in router_test.go to unmarshal the response into the response type and
add an assertion like len(resp.ValidationErrors) == 0 (or resp.ValidationErrors
== nil/empty) for the "valid" cases referenced around expectCode, and make the
same change for the other two places called out so a 200 response cannot hide
validation failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/validation/validate.go`:
- Around line 145-157: The ValidateEntityTypes method currently calls
registry.Validate(cc) without checking registry for nil; add an explicit
nil-check at the start of ValidateEntityTypes (before calling registry.Validate)
and return a multierror.Error containing an explanatory error if registry is
nil. Locate the function ValidateEntityTypes on type contentConfiguration and
ensure the new check returns multierror.Append(nil, fmt.Errorf(...)) (or
similar) so callers get a clear error instead of a panic when EntityTypeRegistry
(registry) is nil.

---

Duplicate comments:
In `@pkg/subroutines/contentconfiguration.go`:
- Around line 105-119: Remove this ContentConfiguration's prior definitions from
r.entityRegistry before calling r.validator.ValidateEntityTypes on
validatedConfig, so validation runs against only the new definitions; implement
a registry call like r.entityRegistry.RemoveDefinitionsFor(instance.GetUID() or
instance.Name) immediately before the ValidateEntityTypes call. After validation
succeeds, replace the old definitions by performing a remove-then-load
transition (call the same RemoveDefinitionsFor(...) before invoking
r.entityRegistry.Load(...) so refcounts don't accumulate and stale types are
cleared). Finally, ensure the ContentConfiguration deletion path/finalizer calls
the same removal logic to unregister this resource's definitions from
r.entityRegistry when the instance is deleted.
- Around line 165-167: The List error is currently swallowed (returning nil) in
the function that calls r.k8sReader.List, causing callers to treat
initialization as successful; change the function that contains the
r.k8sReader.List call to return the encountered error instead of nil (propagate
the error up) so callers (e.g., the registry initialization caller that sets
registryInitDone) can retry or handle failure; locate the List call and the
enclosing function (the method on the receiver r in contentconfiguration.go) and
replace the nil return with return err, ensuring the function signature supports
returning an error or adjust the signature to do so.

---

Nitpick comments:
In `@internal/server/router_test.go`:
- Around line 127-128: The tests that currently only assert expectCode ==
http.StatusOK should also decode the JSON response into the response struct used
in this file (the register response that contains ValidationErrors) and assert
that ValidationErrors is empty; in other words, after the existing status-code
check, call the same decoder used elsewhere in router_test.go to unmarshal the
response into the response type and add an assertion like
len(resp.ValidationErrors) == 0 (or resp.ValidationErrors == nil/empty) for the
"valid" cases referenced around expectCode, and make the same change for the
other two places called out so a 200 response cannot hide validation failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: platform-mesh/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3a9a57ab-e04a-4f7c-9fa7-e807a9aebadb

📥 Commits

Reviewing files that changed from the base of the PR and between 91a5028 and 1ff6198.

📒 Files selected for processing (18)
  • Taskfile.yml
  • cmd/server.go
  • internal/config/config.go
  • internal/controller/contentconfiguration_controller.go
  • internal/server/router.go
  • internal/server/router_test.go
  • internal/server/validate_endpoint.go
  • internal/server/validate_endpoint_test.go
  • pkg/subroutines/contentconfiguration.go
  • pkg/subroutines/contentconfiguration_test.go
  • pkg/subroutines/mocks/mock_Client.go
  • pkg/validation/entitytype.go
  • pkg/validation/entitytype_test.go
  • pkg/validation/interface.go
  • pkg/validation/mocks/mock_ExtensionConfiguration.go
  • pkg/validation/mocks/mock_ResponseWriter.go
  • pkg/validation/validate.go
  • pkg/validation/validate_test.go

Comment thread pkg/validation/validate.go
Three bugs in the entity type validation feature:

1. EntityTypeRegistry.Load was called on every successful reconcile,
   incrementing refcounts unboundedly. Replace Load/Remove with
   LoadForOwner/RemoveOwner/BulkloadWithOwners which track per-CC
   contributions via an owners map — repeated calls for the same owner
   are idempotent and spec changes are applied as a diff.

2. Deleted ContentConfigurations never had their entity types removed
   from the registry. Implement the subroutines.Finalizer interface on
   ContentConfigurationSubroutine so the lifecycle framework adds a
   finalizer to each CC and calls Finalize on deletion, which invokes
   RemoveOwner to clean up registry entries.

3. The server-side registry was a point-in-time snapshot loaded at
   startup. Add a refreshRegistryPeriodically goroutine that re-lists
   ContentConfigurations and calls Bulkload every 5 minutes.

On-behalf-of: @SAP angel.kafazov@sap.com
Signed-off-by: Angel Kafazov <akafazov@cst-bg.net>
@github-actions github-actions Bot added the fix label May 15, 2026
On-behalf-of: @SAP angel.kafazov@sap.com
Signed-off-by: Angel Kafazov <akafazov@cst-bg.net>
On-behalf-of: @SAP angel.kafazov@sap.com
Signed-off-by: Angel Kafazov <akafazov@cst-bg.net>
@github-actions github-actions Bot added the chore label May 15, 2026
@akafazov

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

♻️ Duplicate comments (2)
pkg/subroutines/contentconfiguration.go (2)

75-86: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

List failure still marks the bootstrap as complete.

The previous review flagged that a transient API error during the initial List would leave the registry permanently incomplete. The Store-on-success ordering at line 83 is in place, but initEntityTypeRegistry still swallows the List failure and returns nil (lines 200-203), so the outer caller sets registryInitDone = true and no further reconcile will retry the bootstrap. A subsequent CC reconcile that registers new definitions via LoadForOwner will be operating on a registry that is missing every preexisting entity type defined elsewhere in the cluster, causing valid references to fail validation.

🛡️ Proposed fix
 	var ccList v1alpha1.ContentConfigurationList
 	if err := r.k8sReader.List(ctx, &ccList); err != nil {
-		log.Warn().Err(err).Msg("failed to list ContentConfigurations for entity type registry initialization, registry will be populated incrementally")
-		return nil
+		return fmt.Errorf("list ContentConfigurations for entity type registry initialization: %w", err)
 	}

Also applies to: 194-222

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/subroutines/contentconfiguration.go` around lines 75 - 86,
initEntityTypeRegistry currently swallows transient List errors (see the List
handling around the body of initEntityTypeRegistry) which lets the caller mark
registryInitDone true; change initEntityTypeRegistry so that any failure from
the API List call is returned (not just logged) and only a fully successful
initialization returns nil, ensuring the outer reconcile block that calls
initEntityTypeRegistry (and sets registryInitDone) only marks the registry as
initialized on success; update the error paths in initEntityTypeRegistry (and
any helper it uses for listing/loading, e.g., LoadForOwner) to propagate and
return the error instead of swallowing it so retries occur.

108-133: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Validation runs against the registry that still contains this CC's prior definitions.

r.entityRegistry.Validate(*contentConfiguration) consults r.types, which still holds this owner's previous LoadForOwner contributions because the new contributions are not applied until line 162. A CC whose new revision removes a defineEntity but still references the now-removed type will pass validation against the stale entry and only later have the type silently dropped by LoadForOwner. The Validate helper's selfDefined merge does not compensate for this because the type is no longer self-defined in the new revision. The internal Validate API needs to accept the owner's previous contributions (or expose ValidateForOwner) so the effective set is registry − previous + selfDefined.

#!/bin/bash
# Confirm there is no existing ValidateForOwner / owner-aware Validate variant.
rg -nP --type=go -C3 'func \(.*EntityTypeRegistry\) (Validate|ValidateForOwner)\b'
# Confirm that LoadForOwner is the only path that diffs against r.owners[owner].
rg -nP --type=go -C3 '\br\.owners\[' pkg/validation/

Also applies to: 159-163

🧹 Nitpick comments (7)
pkg/validation/entitytype.go (2)

96-115: 💤 Low value

Consider deduplicating reported errors.

collectReferencedEntityTypes does not deduplicate; a CC that references an unknown entity type on N nodes produces N identical unknown entityType "X" errors. Surfacing one error per distinct unknown type would make the Status.Conditions.Message and /validate response easier to consume without losing information.

♻️ Proposed refactor
 	refs := collectReferencedEntityTypes(cc)
 	var errs []error
+	reported := make(map[string]struct{})
 	for _, ref := range refs {
 		normalized := normalizeEntityType(ref)
 		if normalized == "" {
 			continue
 		}
 		if r.types[normalized] == 0 && !selfDefined[normalized] {
+			if _, seen := reported[normalized]; seen {
+				continue
+			}
+			reported[normalized] = struct{}{}
 			errs = append(errs, fmt.Errorf("unknown entityType %q", ref))
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/validation/entitytype.go` around lines 96 - 115, The Validate method in
EntityTypeRegistry currently appends an error for every referenced occurrence
because collectReferencedEntityTypes isn't deduplicated; update
EntityTypeRegistry.Validate to track seen normalized unknown types (e.g., using
a map[string]struct{}) before appending errors so you only add one
fmt.Errorf("unknown entityType %q", ref) per distinct normalized type; use
normalizeEntityType(ref) for dedup keys and still honor selfDefined and r.types
lookups to suppress known types.

22-48: 💤 Low value

Document the incompatibility between Bulkload and LoadForOwner.

Bulkload resets r.owners to an empty map without seeding owner→types mappings from the input. If LoadForOwner is subsequently called for a CC that was already counted in Bulkload, the registry will double-count that CC's contributions (the previous Bulkload increment cannot be reconciled because no owner key exists for it). Today the two callers are siloed (server uses only Bulkload; operator uses BulkloadWithOwners + LoadForOwner), but a doc comment on Bulkload clarifying that it is incompatible with subsequent per-owner updates would prevent regression.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/validation/entitytype.go` around lines 22 - 48, Bulkload currently resets
r.owners to an empty map and seeds only aggregate counts, which makes it
incompatible with per-owner updates via LoadForOwner because subsequent
per-owner loads will double-count types; add a clear doc comment on the
EntityTypeRegistry.Bulkload method warning that it does not populate owner→types
mappings and should not be used if LoadForOwner or per-owner updates will be
called (recommend using BulkloadWithOwners when owners will be tracked),
referencing Bulkload, BulkloadWithOwners, LoadForOwner and r.owners so
maintainers understand the incompatibility and intended alternative.
cmd/server.go (3)

154-176: 💤 Low value

Bulkload clobbers reference counts on every refresh; ensure the server registry is never used incrementally.

loadRegistryFromCluster calls registry.Bulkload(configs), which (per pkg/validation/entitytype.go) resets r.types and r.owners from scratch. That is safe today because the server-side registry is only fed by this refresh path, but it is a latent footgun: if anyone later adds a code path that calls LoadForOwner or BulkloadWithOwners on this same registry instance, those contributions will be silently wiped every 5 minutes. A short comment on the server-side registry (or a separate constructor like NewBulkOnlyRegistry) would prevent that misuse.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/server.go` around lines 154 - 176, loadRegistryFromCluster calls
registry.Bulkload(configs) which resets internal reference counts on
validation.EntityTypeRegistry and could silently clobber owner-contributed
entries later; add a clear comment on the server-side registry usage near
loadRegistryFromCluster and the registry construction stating it is only
refreshed via Bulkload and must not be used incrementally (i.e. do not call
LoadForOwner or BulkloadWithOwners on this instance), or alternatively create a
distinct constructor (e.g., NewBulkOnlyRegistry) and use that here so intentions
are explicit and accidental incremental updates are prevented.

178-189: 🏗️ Heavy lift

5-minute polling loop leaves up to 5 minutes of stale validation; consider a watch.

The server's only mechanism to learn about new/removed ContentConfiguration resources is a 5-minute ticker, so newly added entity types will be rejected by /validate for up to 5 minutes after they are created in the cluster. This perpetuates the staleness concern from the prior review. A controller-runtime cache.Cache with a watch on ContentConfiguration (and LoadForOwner/RemoveOwner event handlers) would keep the server registry continuously in sync with O(1) latency, and Bulkload would only be needed as a startup seed.

If a watch is out of scope for this PR, please at least document the staleness window and confirm the chosen interval is acceptable to the issue tracked in #611.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/server.go` around lines 178 - 189, refreshRegistryPeriodically currently
polls every 5 minutes using loadRegistryFromCluster which allows up to 5 minutes
of stale EntityTypeRegistry state for ContentConfiguration changes; replace or
supplement this polling with a controller-runtime cache.Cache watch on
ContentConfiguration and hook handlers (e.g., LoadForOwner/RemoveOwner
semantics) to update the registry in real time, keeping the registry pointer in
refreshRegistryPeriodically (or the registry update path used by
loadRegistryFromCluster) in sync on Add/Update/Delete events; if implementing a
watch is out of scope, document the staleness window in comments near
refreshRegistryPeriodically and confirm the 5-minute interval meets issue `#611`
requirements and why Bulkload remains only a startup seed.

128-152: ⚡ Quick win

Prefer ctrl.GetConfig() over rolling a custom kubeconfig/in-cluster selector.

controller-runtime's ctrl.GetConfig() already implements the standard precedence (in-cluster → KUBECONFIG~/.kube/config → recommended search paths) and is the established convention across this repository's operator entrypoint. Reimplementing the selection logic here diverges from that convention and silently inverts precedence: KUBECONFIG is honored even when running in-cluster, which can cause the server to talk to the wrong API server if the env var leaks into a pod.

♻️ Proposed refactor
-func buildServerK8sClient() client.Reader { // coverage-ignore
-	kubeconfigPath := os.Getenv("KUBECONFIG")
-	var restCfg *rest.Config
-	var err error
-	if kubeconfigPath != "" {
-		restCfg, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath)
-		if err != nil {
-			log.Warn().Err(err).Msg("failed to load kubeconfig, entity type validation will only recognize 'global'")
-			return nil
-		}
-	} else {
-		restCfg, err = rest.InClusterConfig()
-		if err != nil {
-			log.Warn().Err(err).Msg("not running in cluster, entity type validation will only recognize 'global'")
-			return nil
-		}
-	}
-
-	k8sClient, err := client.New(restCfg, client.Options{Scheme: scheme})
+func buildServerK8sClient() client.Reader { // coverage-ignore
+	restCfg, err := ctrl.GetConfig()
+	if err != nil {
+		log.Warn().Err(err).Msg("failed to resolve rest config, entity type validation will only recognize 'global'")
+		return nil
+	}
+	k8sClient, err := client.New(restCfg, client.Options{Scheme: scheme})

v1alpha1 is properly registered in the scheme (confirmed in cmd/root.go init function, line 45), so the k8sClient.List() call for v1alpha1.ContentConfigurationList will work correctly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/server.go` around lines 128 - 152, The buildServerK8sClient function
reimplements kubeconfig/in-cluster precedence and honors KUBECONFIG before
in-cluster; replace its manual selection with controller-runtime's
ctrl.GetConfig() to use the standard precedence. Modify buildServerK8sClient to
call ctrl.GetConfig(), handle and log any error similarly, then pass the
returned *rest.Config into client.New(..., client.Options{Scheme: scheme}) and
return that client (or nil on error) so existing uses of k8sClient and the
registered v1alpha1 scheme continue to work.
internal/server/router_test.go (1)

149-156: ⚡ Quick win

Assert empty validation errors when expectValidation is false.

The "valid entityType with registry" and "known entityType project with registry" cases only verify the HTTP status code. Since /validate returns 200 OK regardless of validation outcome, these cases would pass even if entity-type validation incorrectly produced errors. Decoding the response and asserting assert.Empty(t, resp.ValidationErrors) on the success cases (mirroring what TestCreateRouter_SelfReferencingCC already does) would make the regression coverage meaningful.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/server/router_test.go` around lines 149 - 156, The two table-driven
test cases named "valid entityType with registry" and "known entityType project
with registry" only check HTTP status but not the validation payload; update
their test logic in router_test.go to decode the /validate response body (the
same response struct used in TestCreateRouter_SelfReferencingCC) and add an
assertion that resp.ValidationErrors is empty when expectValidation is false.
Locate the handling for those cases in the test loop (the entries with name
values above) and mirror the decoding + assert.Empty(t, resp.ValidationErrors)
used by TestCreateRouter_SelfReferencingCC so a 200 OK with validation errors
will fail the test.
pkg/subroutines/contentconfiguration.go (1)

187-192: 💤 Low value

ownerKey fallback risks collisions across namespaces.

When UID is empty (rare in practice, but possible in unit tests or apply-server short-circuits), the fallback is cc.Namespace + "/" + cc.Name. Two resources whose namespace+"/"+name happen to collide because the namespace contains a / cannot occur in Kubernetes, but mixing UID-keyed entries from real reconciles with namespace/name-keyed entries from tests within the same registry yields divergent owner identities for the same logical object. Consider documenting that production callers must always provide objects with UIDs populated, or unconditionally key by namespace/name to keep the identifier stable across the resource's lifecycle.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/subroutines/contentconfiguration.go` around lines 187 - 192, The ownerKey
function currently falls back to namespace/name only when UID is empty, causing
mixed UID-keyed and name-keyed entries to diverge; change ownerKey (in
pkg/subroutines/contentconfiguration.go) to always return a stable
namespace/name key (cc.Namespace + "/" + cc.Name) for
v1alpha1.ContentConfiguration so identity remains consistent across reconciles
and tests, and update any callers/tests that relied on UID-based keys to use the
namespace/name convention instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/subroutines/contentconfiguration.go`:
- Around line 168-185: Finalize currently returns early when r.entityRegistry is
nil, leaving the entityTypeFinalizerName present and blocking deletion; update
Finalize to always attempt to remove the finalizer string (using
controllerutil.RemoveFinalizer or equivalent) from the passed obj even when
r.entityRegistry is nil, then perform the existing cleanup
(r.entityRegistry.RemoveOwner(ownerKey(instance))) only if entityRegistry is
non-nil, and finally return subroutines.OK() so the subroutines framework can
update the object and clear the finalizer; keep Finalizers() behavior unchanged
but ensure Finalize handles finalizer removal unconditionally for
ContentConfiguration instances.

---

Duplicate comments:
In `@pkg/subroutines/contentconfiguration.go`:
- Around line 75-86: initEntityTypeRegistry currently swallows transient List
errors (see the List handling around the body of initEntityTypeRegistry) which
lets the caller mark registryInitDone true; change initEntityTypeRegistry so
that any failure from the API List call is returned (not just logged) and only a
fully successful initialization returns nil, ensuring the outer reconcile block
that calls initEntityTypeRegistry (and sets registryInitDone) only marks the
registry as initialized on success; update the error paths in
initEntityTypeRegistry (and any helper it uses for listing/loading, e.g.,
LoadForOwner) to propagate and return the error instead of swallowing it so
retries occur.

---

Nitpick comments:
In `@cmd/server.go`:
- Around line 154-176: loadRegistryFromCluster calls registry.Bulkload(configs)
which resets internal reference counts on validation.EntityTypeRegistry and
could silently clobber owner-contributed entries later; add a clear comment on
the server-side registry usage near loadRegistryFromCluster and the registry
construction stating it is only refreshed via Bulkload and must not be used
incrementally (i.e. do not call LoadForOwner or BulkloadWithOwners on this
instance), or alternatively create a distinct constructor (e.g.,
NewBulkOnlyRegistry) and use that here so intentions are explicit and accidental
incremental updates are prevented.
- Around line 178-189: refreshRegistryPeriodically currently polls every 5
minutes using loadRegistryFromCluster which allows up to 5 minutes of stale
EntityTypeRegistry state for ContentConfiguration changes; replace or supplement
this polling with a controller-runtime cache.Cache watch on ContentConfiguration
and hook handlers (e.g., LoadForOwner/RemoveOwner semantics) to update the
registry in real time, keeping the registry pointer in
refreshRegistryPeriodically (or the registry update path used by
loadRegistryFromCluster) in sync on Add/Update/Delete events; if implementing a
watch is out of scope, document the staleness window in comments near
refreshRegistryPeriodically and confirm the 5-minute interval meets issue `#611`
requirements and why Bulkload remains only a startup seed.
- Around line 128-152: The buildServerK8sClient function reimplements
kubeconfig/in-cluster precedence and honors KUBECONFIG before in-cluster;
replace its manual selection with controller-runtime's ctrl.GetConfig() to use
the standard precedence. Modify buildServerK8sClient to call ctrl.GetConfig(),
handle and log any error similarly, then pass the returned *rest.Config into
client.New(..., client.Options{Scheme: scheme}) and return that client (or nil
on error) so existing uses of k8sClient and the registered v1alpha1 scheme
continue to work.

In `@internal/server/router_test.go`:
- Around line 149-156: The two table-driven test cases named "valid entityType
with registry" and "known entityType project with registry" only check HTTP
status but not the validation payload; update their test logic in router_test.go
to decode the /validate response body (the same response struct used in
TestCreateRouter_SelfReferencingCC) and add an assertion that
resp.ValidationErrors is empty when expectValidation is false. Locate the
handling for those cases in the test loop (the entries with name values above)
and mirror the decoding + assert.Empty(t, resp.ValidationErrors) used by
TestCreateRouter_SelfReferencingCC so a 200 OK with validation errors will fail
the test.

In `@pkg/subroutines/contentconfiguration.go`:
- Around line 187-192: The ownerKey function currently falls back to
namespace/name only when UID is empty, causing mixed UID-keyed and name-keyed
entries to diverge; change ownerKey (in pkg/subroutines/contentconfiguration.go)
to always return a stable namespace/name key (cc.Namespace + "/" + cc.Name) for
v1alpha1.ContentConfiguration so identity remains consistent across reconciles
and tests, and update any callers/tests that relied on UID-based keys to use the
namespace/name convention instead.

In `@pkg/validation/entitytype.go`:
- Around line 96-115: The Validate method in EntityTypeRegistry currently
appends an error for every referenced occurrence because
collectReferencedEntityTypes isn't deduplicated; update
EntityTypeRegistry.Validate to track seen normalized unknown types (e.g., using
a map[string]struct{}) before appending errors so you only add one
fmt.Errorf("unknown entityType %q", ref) per distinct normalized type; use
normalizeEntityType(ref) for dedup keys and still honor selfDefined and r.types
lookups to suppress known types.
- Around line 22-48: Bulkload currently resets r.owners to an empty map and
seeds only aggregate counts, which makes it incompatible with per-owner updates
via LoadForOwner because subsequent per-owner loads will double-count types; add
a clear doc comment on the EntityTypeRegistry.Bulkload method warning that it
does not populate owner→types mappings and should not be used if LoadForOwner or
per-owner updates will be called (recommend using BulkloadWithOwners when owners
will be tracked), referencing Bulkload, BulkloadWithOwners, LoadForOwner and
r.owners so maintainers understand the incompatibility and intended alternative.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: platform-mesh/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 20f69a7d-e9f5-4eef-b224-3dfc44352ef9

📥 Commits

Reviewing files that changed from the base of the PR and between 91a5028 and d0832df.

📒 Files selected for processing (18)
  • Taskfile.yml
  • cmd/server.go
  • internal/config/config.go
  • internal/controller/contentconfiguration_controller.go
  • internal/server/router.go
  • internal/server/router_test.go
  • internal/server/validate_endpoint.go
  • internal/server/validate_endpoint_test.go
  • pkg/subroutines/contentconfiguration.go
  • pkg/subroutines/contentconfiguration_test.go
  • pkg/subroutines/mocks/mock_Client.go
  • pkg/validation/entitytype.go
  • pkg/validation/entitytype_test.go
  • pkg/validation/interface.go
  • pkg/validation/mocks/mock_ExtensionConfiguration.go
  • pkg/validation/mocks/mock_ResponseWriter.go
  • pkg/validation/validate.go
  • pkg/validation/validate_test.go

Comment thread pkg/subroutines/contentconfiguration.go
On-behalf-of: @SAP angel.kafazov@sap.com
Signed-off-by: Angel Kafazov <akafazov@cst-bg.net>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants