Skip to content

HYPERFLEET-538 - feat: CEL-based condition mapping engine - #315

Open
ldornele wants to merge 17 commits into
openshift-hyperfleet:mainfrom
ldornele:HYPERFLEET-538
Open

HYPERFLEET-538 - feat: CEL-based condition mapping engine#315
ldornele wants to merge 17 commits into
openshift-hyperfleet:mainfrom
ldornele:HYPERFLEET-538

Conversation

@ldornele

Copy link
Copy Markdown
Contributor

Summary

Implements a CEL-based condition mapping engine that allows declarative exposure of adapter-specific conditions in the public API status.conditions array without code changes. Operators can now configure
YAML-based mapping rules that compile at startup (fail-fast) and evaluate at runtime to transform rich adapter status into public conditions.

Motivation

Currently, the HyperFleet API only exposes aggregated conditions (Reconciled, LastKnownReconciled) and per-adapter success flags (AdapterXSuccessful) in the public API. Rich provider-specific conditions
from adapters (e.g., Landing Zone namespace readiness, Validation quota status) exist in the internal /statuses endpoint but are not accessible to external consumers (CLI, UI, customer integrations).

This creates a gap: operators cannot surface meaningful adapter state (quota limits, policy validation, namespace readiness) to end users without API code changes for each new condition type.

This PR closes that gap.

What Changed

🎯 Core Features

1. Config-driven Mapping Rules (pkg/config/conditions.go)

  • YAML schema: conditions.clusters.<ConditionType> and conditions.nodepools.<ConditionType>
  • Each rule has:
    • when: CEL expression (boolean) - when to generate the condition
    • output.status/reason/message: CEL expressions for field values
  • Startup validation:
    • ✅ Rejects reserved types (Reconciled, LastKnownReconciled)
    • ✅ Validates field length constraints (128/256/2048 chars)
    • ✅ Pre-compiles CEL expressions (Parse → Check → Program)
    • Fails fast on invalid config

2. CEL Evaluation Engine (pkg/services/condition_mapper.go)

  • Compiles rules once at startup, evaluates on every status update
  • CEL context variables:
    • statuses: array of adapter statuses (Unknown conditions filtered)
    • resource: full cluster/nodepool object (sensitive fields masked)
    • env: environment variables (reserved for future use)
  • Custom CEL functions (pkg/util/cel.go):
    • toJson(value): marshal to JSON string
    • dig(target, "dot.path"): safe nested navigation (map keys + array indices)
  • Error handling:
    • Parse/Check/Program errors → fail at startup
    • Eval errors → log warning, skip condition (graceful degradation)
  • Field validation:
    • Type/Reason length exceeded → skip condition
    • Message length exceeded → truncate (preserving UTF-8 boundaries)

3. Integration (pkg/services/resource.go, pkg/services/aggregation.go)

  • Triggers condition recomputation when mapper configured OR adapter statuses change
  • Appends mapped conditions to status.conditions array (after Reconciled/LastKnownReconciled)
  • Backward compatible:
    • nil mapper → no CEL mapping (existing behavior)
    • Empty config → feature disabled
    • Preserves existing condition indices

4. Security (pkg/util/mask_sensitive.go)

  • Defense-in-depth: MaskSensitiveFields() applied to both resource and data context variables
  • Regex-based detection: AWS keys, JWT tokens, base64 secrets, etc.
  • Prevents CEL expressions from accidentally logging credentials
  • Follow-on work (HYPERFLEET-1128): enhanced data protection (non-blocking)

5. Performance Optimizations

  • Early return when Unknown condition found (PERF-03) - skips JSON parsing + masking
  • CEL cost limits (10,000 units) - prevents CPU spikes from pathological expressions
  • Nil guard in buildActivation() - skips nil adapter statuses

📦 Files Changed

New files (11):

  • pkg/config/conditions.go - Config schema & validation
  • pkg/config/conditions_test.go - 7 validation tests
  • pkg/services/condition_mapper.go - CEL mapper implementation
  • pkg/services/condition_mapper_test.go - 9 mapper tests
  • pkg/util/cel.go - CEL environment & custom functions
  • pkg/util/cel_test.go - 5 CEL function tests
  • pkg/util/mask_sensitive.go - Sensitive data masking
  • pkg/util/mask_sensitive_test.go - Masking tests
  • pkg/util/naming.go - Adapter name → condition type helper
  • pkg/util/naming_test.go - Naming tests
  • test/integration/condition_mapping_test.go - 3 integration tests

Modified files (10):

  • pkg/config/config.go - Add Conditions field to ApplicationConfig
  • pkg/config/loader.go - Call Conditions.Validate() at startup
  • pkg/services/resource.go - Create mapper, trigger recomputation
  • pkg/services/aggregation.go - Append mapped conditions
  • pkg/services/aggregation_test.go - Updated test expectations
  • pkg/services/resource_test.go - Added mapper integration test
  • plugins/resources/plugin.go - Pass config to service constructor
  • configs/config.yaml.example - CEL mapping documentation + example
  • go.mod / go.sum - Add github.com/google/cel-go v0.26.1

Example Usage

config.yaml:

conditions:
  clusters:
    LandingZoneReady:
      when:
        expression: 'statuses.exists(s, s.adapter == "landing-zone" && s.conditions.exists(c, c.type == "NamespaceReady"))'
      output:
        status:
          expression: 'statuses.filter(s, s.adapter == "landing-zone")[0].conditions.filter(c, c.type == "NamespaceReady")[0].status'
        reason:
          expression: 'statuses.filter(s, s.adapter == "landing-zone")[0].conditions.filter(c, c.type == "NamespaceReady")[0].reason'
        message:
          expression: '"Landing zone: " + statuses.filter(s, s.adapter == "landing-zone")[0].conditions.filter(c, c.type == "NamespaceReady")[0].message'

Result:

When landing-zone adapter reports NamespaceReady=True:

GET /api/hyperfleet/v1/clusters/{id}

{
  "status": {
    "conditions": [
      {"type": "Reconciled", "status": "True", ...},
      {"type": "LastKnownReconciled", "status": "True", ...},
      {"type": "LandingZoneSuccessful", "status": "True", ...},
      {"type": "LandingZoneReady", "status": "True", "reason": "NamespaceCreated", "message": "Landing zone: Namespace ready", ...}
    ]
  }
}

Testing

✅ Unit Tests (24 tests)

Config validation (pkg/config/conditions_test.go - 7 tests):

  • ✅ Valid config loads successfully
  • ✅ Invalid CEL syntax rejected at startup
  • ✅ Reserved condition types rejected (Reconciled, LastKnownReconciled)
  • ✅ Field length constraints validated
  • ✅ Duplicate condition types rejected
  • ✅ Cross-entity validation (clusters vs nodepools)
  • ✅ When expression returning false is allowed

Mapper logic (pkg/services/condition_mapper_test.go - 9 tests):

  • ✅ Single condition mapping (when=true → output evaluated)
  • ✅ When=false → no condition generated
  • ✅ Unknown adapter conditions filtered
  • ✅ Cross-adapter aggregation (multiple adapters in expression)
  • ✅ Field truncation (type/reason skip, message truncates)
  • ✅ Invalid status string ("Maybe" instead of "True"/"False")
  • ✅ Non-boolean when expression (returns string instead of bool)
  • ✅ Concurrent Apply() calls (goroutine-safe via channel-based result collection)
  • ✅ Nil adapter status handling (skipped safely)

CEL functions (pkg/util/cel_test.go - 5 tests):

  • ✅ dig() map navigation
  • ✅ dig() array navigation with indices
  • ✅ toJson() marshaling
  • ✅ CEL variable constants prevent typos (QUAL-01)
  • ✅ env.Check() detects undefined variables

Masking (pkg/util/mask_sensitive_test.go - tests):

  • ✅ AWS access keys masked
  • ✅ JWT tokens masked
  • ✅ Base64 secrets masked
  • ✅ Nested fields traversed

✅ Integration Tests (3 tests)

End-to-end (test/integration/condition_mapping_test.go):

  • ✅ Adapter reports condition → PUT /statuses → GET /clusters shows mapped condition (skipped by default, requires HYPERFLEET_TEST_CONDITION_MAPPING=1)
  • ✅ Multiple rules active simultaneously (skipped by default)
  • ✅ No config → existing behavior preserved (always runs)

✅ Test Results

DONE 1476 tests in 21.998s
✅ 100% passing

Backward Compatibility

✅ No breaking changes

  • Empty config (conditions: {}) → feature disabled, existing behavior preserved
  • Nil mapper → graceful degradation (logs warning, continues without CEL)
  • Existing conditions (Reconciled, LastKnownReconciled, AdapterXSuccessful) unchanged

Rollback Plan

If issues discovered post-merge:

  1. Set conditions: {} in config.yaml (empty map)
  2. Restart API
  3. Mapped conditions disappear, core functionality preserved

Security Considerations

✅ Defense-in-depth implemented:

  • util.MaskSensitiveFields() applied to resource and data CEL variables
  • Prevents CEL expressions from leaking credentials via logs/messages
  • Documented in config.yaml.example

⚠️ Follow-on work (HYPERFLEET-1128):

  • Enhanced data protection (field-level allowlist/blocklist)
  • Explicitly non-blocking for this MVP per ticket description

✅ CEL security:

  • Cost limits (10,000 units) prevent CPU exhaustion
  • No filesystem/network access from CEL
  • Static compilation at startup (no runtime code injection)

Checklist

  • Tests added/updated (24 unit + 3 integration)
  • All tests passing (1476/1476)
  • Documentation updated (config.yaml.example)
  • Backward compatible (graceful degradation)
  • Security reviewed (masking implemented)
  • Performance considered (cost limits, early returns)
  • go.mod/go.sum updated (cel-go v0.26.1)
  • Code formatted (gofmt applied)
  • Commit message follows convention

Notes for Reviewers

🔍 Key Files to Review

Core logic:

  1. pkg/services/condition_mapper.go - Main CEL evaluation engine
  2. pkg/config/conditions.go - Config schema & validation
  3. pkg/services/aggregation.go:640-652 - Integration point

Security:
4. pkg/util/mask_sensitive.go - Sensitive data protection

Testing:
5. pkg/services/condition_mapper_test.go - Comprehensive test coverage

@openshift-ci
openshift-ci Bot requested a review from jsell-rh July 27, 2026 18:12
@openshift-ci

openshift-ci Bot commented Jul 27, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign aredenba-rh for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci
openshift-ci Bot requested a review from mliptak0 July 27, 2026 18:12
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added configurable condition mapping using CEL expressions for translating adapter statuses into resource conditions.
    • Added support for mapping multiple conditions, filtering unknown conditions, preserving timestamps, and updating generation information.
    • Added built-in helpers for accessing nested data and serializing resources in mapping rules.
  • Bug Fixes
    • Sensitive values are redacted before condition rules are evaluated.
    • Invalid mappings now fail validation early with clear errors.
    • Oversized condition fields are safely truncated while preserving valid text.

Walkthrough

Adds CEL-based condition mappings with registry-time validation, reserved condition checks, and compile-time cost limits. Adds CEL helpers for bounded JSON conversion, nested lookup, sensitive-data masking, and adapter condition naming. Adds ConditionMapper evaluation with deterministic rules, timestamp preservation, truncation, Unknown filtering, and runtime errors. Integrates mapped conditions into resource processing with rollback on failures. Adds unit, concurrency, benchmark, service, registry, and integration coverage.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Adapter
  participant ResourceService
  participant ConditionMapper
  participant CEL
  Adapter->>ResourceService: Submit adapter conditions and data
  ResourceService->>ConditionMapper: Apply resource state and prior conditions
  ConditionMapper->>CEL: Evaluate compiled mapping expressions
  CEL-->>ConditionMapper: Return mapped condition fields
  ConditionMapper-->>ResourceService: Return mapped conditions or error
  ResourceService-->>Adapter: Persist result or roll back
Loading

Suggested reviewers: jsell-rh, kuudori

🚥 Pre-merge checks | ✅ 10 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (10 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the CEL-based condition mapping engine and includes the related issue identifier.
Description check ✅ Passed The description directly explains the CEL mapping engine, configuration, integration, security controls, testing, and backward compatibility.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Sec-02: Secrets In Log Output ✅ Passed Feature-added non-test logs contain only resource_kind, condition_type, adapter, generation, length, and parse errors; no token, password, credential, or secret fields or interpolated values were f...
No Hardcoded Secrets ✅ Passed No CWE-798 finding: added config has no secret values or long base64 strings; source has no secret assignments; credential-shaped literals occur only in test fixtures.
No Weak Cryptography ✅ Passed The main-to-HEAD diff adds no crypto/md5, crypto/des, crypto/rc4, SHA-1, ECB, custom crypto, or secret comparisons; crypto-related additions are masking patterns and test data only.
No Injection Vectors ✅ Passed The PR adds no production exec.Command, template.HTML, yaml.Unmarshal, or query-construction sink; fmt.Sprintf additions are error/value formatting, and query formatting is pre-existing.
No Privileged Containers ✅ Passed No prohibited production setting found (CWE-250). Dockerfile USER root is limited to make installation and switches to non-root; Helm defaults enforce non-root execution.
No Pii Or Sensitive Data In Logs ✅ Passed Feature-added logs emit only resource kind, condition type, adapter, generation, and parse/marshal errors; no PII, bodies, credentials, hostnames, or session data were logged (CWE-532 not observed).
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

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

@hyperfleet-ci-bot

hyperfleet-ci-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

Risk Score: 3 — risk/medium

Signal Detail Points
PR size 5345 lines (>500) +2
Sensitive paths none +0
Test coverage Missing tests for: pkg/config +1

Computed by hyperfleet-risk-scorer

@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: 7

🧹 Nitpick comments (9)
pkg/services/condition_mapper_test.go (1)

1261-1392: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Three subtests are parented under the wrong test function, and one assertion is vacuous.

"invalid status string from CEL expression", "non-boolean when expression return type", and "concurrent Apply() access from multiple goroutines" live inside TestTruncateUTF8. They exercise Apply(), not truncation — the test name no longer describes the scenario, and a -run TestTruncateUTF8 filter now drags in a 10-goroutine race test.

Separately, Line 1387: result.err is never assigned by the producer goroutine, so Expect(res.err).NotTo(HaveOccurred()) can never fail. Drop the field or make Apply failures observable.

♻️ Split into dedicated test functions
+}
+
+func TestConditionMapper_InvalidOutputs(t *testing.T) {
 	t.Run("invalid status string from CEL expression", func(t *testing.T) {
 	t.Run("concurrent Apply() access from multiple goroutines", func(t *testing.T) {

Move to its own TestConditionMapper_ConcurrentApply, and reduce the channel payload to []api.ResourceCondition:

-		type result struct {
-			conditions []api.ResourceCondition
-			err        error
-		}
-		results := make(chan result, numGoroutines)
+		results := make(chan []api.ResourceCondition, numGoroutines)

As per coding guidelines: "Test names describe the scenario, not the implementation."

🤖 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/services/condition_mapper_test.go` around lines 1261 - 1392, Move the
three Apply-focused subtests out of TestTruncateUTF8 into appropriately named
dedicated test functions, including TestConditionMapper_ConcurrentApply for the
concurrency case, so truncation filters remain scoped correctly. In the
concurrent test, remove the unused result.err field and its assertion, and
send/receive only []api.ResourceCondition through the channel while preserving
the existing result assertions.

Source: Path instructions

pkg/util/cel.go (1)

55-70: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Size guard fires after the allocation it claims to prevent (CWE-770 adjacent).

json.Marshal fully materializes the payload before Line 65 rejects it, so the comment on Line 62 ("prevent unbounded intermediate allocations") does not hold. Attacker-influenced adapter data blobs land in this path via the CEL activation. The CEL cost limit bounds expression complexity, not marshaled output size.

Not exploitable at current JSONB sizes, but either use a size-capped encoder or drop the misleading rationale.

♻️ Bound the write instead of measuring after the fact
 func toJSONFunc(val ref.Val) ref.Val {
 	v := val.Value()
-	data, err := json.Marshal(v)
-	if err != nil {
-		return types.NewErr("toJson: %v", err)
-	}
-
-	// Size guard: prevent unbounded intermediate allocations from large payloads
-	// Limit matches Kubernetes ConfigMap max size (1MB) as a reasonable upper bound
-	const maxJSONSize = 1 * 1024 * 1024 // 1MB
-	if len(data) > maxJSONSize {
-		return types.NewErr("toJson: output exceeds 1MB limit (%d bytes)", len(data))
-	}
-
-	return types.String(string(data))
+	// Limit matches Kubernetes ConfigMap max size (1MB) as a reasonable upper bound
+	const maxJSONSize = 1 * 1024 * 1024 // 1MB
+
+	var buf bytes.Buffer
+	buf.Grow(1024)
+	if err := json.NewEncoder(&limitedWriter{w: &buf, n: maxJSONSize}).Encode(v); err != nil {
+		return types.NewErr("toJson: %v", err)
+	}
+	return types.String(strings.TrimRight(buf.String(), "\n"))
 }
🤖 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/util/cel.go` around lines 55 - 70, Update toJSONFunc so serialization
enforces the 1MB limit while writing, rather than calling json.Marshal and
checking the fully allocated result afterward. Use a size-capped JSON encoder or
equivalent bounded writer, preserve the existing toJson error behavior, and
retain the maxJSONSize limit without claiming it prevents allocations it cannot
prevent.
pkg/services/resource_test.go (1)

3160-3232: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Only the happy path is covered for the new mapper integration.

Two gaps worth closing here, both cheap given the helper you just added:

  • when evaluating false → no CustomReady condition emitted (verifies rules don't leak conditions unconditionally).
  • A rule keyed with a type that collides with a built-in (Reconciled) or an adapter-derived type (ValidationSuccessful) — this is the scenario behind the collision issue flagged on pkg/services/resource.go Line 690-699, and a test would pin whatever behavior you settle on.

As per coding guidelines: "Error paths SHOULD be tested, not just happy paths."

🤖 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/services/resource_test.go` around lines 3160 - 3232, Extend
TestResourceService_ConditionMapper_IntegrationPath with a false-evaluating when
expression and assert no CustomReady condition is emitted. Add collision
coverage using mapped types Reconciled and ValidationSuccessful, asserting the
intended behavior from the condition-mapping logic without unintended overwrites
or duplicates. Reuse the existing test helpers and verify both outcomes through
rcDao.conditions.

Source: Path instructions

pkg/services/condition_mapper.go (1)

205-209: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicate condition-type length check here. pkg/config/conditions.go already rejects overlong condition types during ConditionsConfig.Validate(), so this branch is redundant in the normal startup path. If NewConditionMapper can still be called without validated config, return the error instead of swallowing it and dropping the condition. (CWE-391)

🤖 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/services/condition_mapper.go` around lines 205 - 209, Update the error
handling around validateFieldLengths in NewConditionMapper’s condition-mapping
flow: remove the redundant condition-type length validation there, relying on
ConditionsConfig.Validate() for validated startup configuration. If validation
can still fail for unvalidated configurations, propagate or return the error
instead of silently returning nil, nil and dropping the condition.
pkg/util/naming_test.go (1)

10-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add degenerate-input cases: "", "-adapter", "multi--word".

Empty or dash-edged adapter names yield "Successful" / "AdapterSuccessful", which feeds buildReservedConditionTypes (pkg/config/conditions.go:115-140) and can silently reserve an unintended type.

🤖 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/util/naming_test.go` around lines 10 - 22, Extend the testCases table in
the naming tests with degenerate adapter inputs "", "-adapter", and
"multi--word", asserting the intended naming behavior for each. Use the existing
naming function under test and ensure these cases verify that empty or
dash-edged names do not produce unintended reserved condition types consumed by
buildReservedConditionTypes.
pkg/config/conditions.go (1)

84-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deduplicate the cluster/nodepool validation loops.

Two identical sort-and-iterate blocks. Extract a helper so adding a third entity kind is a one-line change instead of a copy-paste.

♻️ Proposed refactor
-	// Validate cluster mappings (sort keys for deterministic error messages)
-	clusterKeys := make([]string, 0, len(c.Clusters))
-	for condType := range c.Clusters {
-		clusterKeys = append(clusterKeys, condType)
-	}
-	sort.Strings(clusterKeys)
-	for _, condType := range clusterKeys {
-		if err := validateConditionMapping("clusters", condType, c.Clusters[condType], reserved, env); err != nil {
-			return err
-		}
-	}
-
-	// Validate nodepool mappings (sort keys for deterministic error messages)
-	nodepoolKeys := make([]string, 0, len(c.NodePools))
-	for condType := range c.NodePools {
-		nodepoolKeys = append(nodepoolKeys, condType)
-	}
-	sort.Strings(nodepoolKeys)
-	for _, condType := range nodepoolKeys {
-		if err := validateConditionMapping("nodepools", condType, c.NodePools[condType], reserved, env); err != nil {
-			return err
-		}
-	}
-
-	return nil
+	if err := validateRuleSet("clusters", c.Clusters, reserved, env); err != nil {
+		return err
+	}
+	return validateRuleSet("nodepools", c.NodePools, reserved, env)
+}
+
+// validateRuleSet validates a rule map with deterministic (sorted) error ordering.
+func validateRuleSet(resourceType string, rules map[string]ConditionMappingRule, reserved map[string]bool, env *cel.Env) error {
+	keys := make([]string, 0, len(rules))
+	for condType := range rules {
+		keys = append(keys, condType)
+	}
+	sort.Strings(keys)
+	for _, condType := range keys {
+		if err := validateConditionMapping(resourceType, condType, rules[condType], reserved, env); err != nil {
+			return err
+		}
+	}
+	return nil
 }
🤖 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/config/conditions.go` around lines 84 - 106, Deduplicate the repeated
validation logic in the cluster and nodepool sections of the configuration
validation flow. Extract a helper that accepts the entity name and
condition-mapping collection, sorts its keys, and calls validateConditionMapping
for each entry while propagating errors; invoke it for both c.Clusters and
c.NodePools.
pkg/config/conditions_test.go (1)

188-208: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add invalid-expression cases for output.reason and output.message.

Only when and output.status are exercised; the two remaining validateCELExpression calls (pkg/config/conditions.go:167-172) have no coverage, so a typo in either field-name string would go unnoticed.

🤖 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/config/conditions_test.go` around lines 188 - 208, Extend the condition
validation tests alongside the existing invalid output status case to cover
malformed CEL expressions in output.reason and output.message. Add separate
configurations using incomplete expressions for each field, and assert wantError
is true with the existing “invalid CEL expression” error expectation, exercising
the validateCELExpression calls for both fields.
pkg/util/cel_test.go (2)

88-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the parse/check/program/eval pipeline into a t.Helper() helper.

The same four-step block is copied in three tests. A single helper keeps the tests aligned with compileExpression if the pipeline changes.

Also applies to: 175-198

🤖 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/util/cel_test.go` around lines 88 - 115, Extract the repeated CEL parse,
check, program creation, and evaluation sequence from the affected tests into a
shared test helper marked with t.Helper(). Have the helper accept the expression
and resource data, return the evaluated output and error, and update all three
tests—including the block around the additional referenced lines—to use it while
preserving their existing assertions and CEL cost limit.

199-227: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

toJson 1MB size guard is untested.

toJSONFunc (pkg/util/cel.go:55-70) rejects payloads >1MB — that's the DoS containment boundary for CEL-driven serialization of adapter data (CWE-400). No test asserts it. Add a case feeding a large map and asserting the exceeds 1MB limit error surfaces as an eval error. Same gap applies to the CELCostLimit rejection path.

As per path instructions: "Error paths SHOULD be tested, not just happy paths".

🤖 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/util/cel_test.go` around lines 199 - 227, Extend TestToJsonFunc with
error-path cases for the toJSONFunc 1MB payload guard and CELCostLimit
rejection. Feed toJson a sufficiently large map and assert evaluation returns an
error containing “exceeds 1MB limit”; separately exercise a program that exceeds
CELCostLimit and assert the evaluation surfaces the cost-limit error.

Source: Path instructions

🤖 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 `@configs/config.yaml.example`:
- Around line 166-168: Update the TODO near the CEL env context configuration to
reference the specific HYPERFLEET ticket ID for tracking env variable support,
replacing the instruction to create a ticket; preserve the existing description
of the planned env.REGION and env.ENVIRONMENT access.
- Around line 177-180: Update the “type” field-length constraint in the
configuration example to document the implemented behavior: values exceeding 128
bytes cause validation to return an error and prevent startup, rather than being
skipped. Keep the existing truncation descriptions for “reason” and “message”
unchanged.

In `@go.mod`:
- Line 15: Update the direct github.com/google/cel-go dependency in go.mod from
v0.26.1 to v0.29.0 or newer, and refresh the module checksums or related
dependency metadata as required while preserving build compatibility.

In `@pkg/services/condition_mapper_test.go`:
- Around line 369-372: Replace the pullSecret value in the test data map near
data2 with a non-decodable placeholder that cannot represent auth JSON, and
update the corresponding value at the referenced later test location
consistently. Keep the test’s pullSecret coverage intact while removing the
base64-like secret pattern.

In `@pkg/services/resource.go`:
- Around line 612-617: Reduce unnecessary condition recomputation in
pkg/services/resource.go lines 612-617 by avoiding unconditional
hasMapper-triggered aggregation; run it only for triggerAggregation or when the
incoming status conditions/data differ from existingStatus. In
pkg/services/condition_mapper.go lines 376-393, change Apply so the resource
JSON marshal and MaskSensitiveFields traversal are performed lazily or once per
Apply call and reused across rule evaluations, rather than rebuilt for each
rule. Use the existing ProcessAdapterStatus and Apply flows as the anchors for
these changes.

In `@test/integration/condition_mapping_test.go`:
- Around line 255-290: The condition-mapping test currently uses PolicyValid for
both the adapter input and mapped output, making the assertion ineffective. In
the test setup and corresponding expected mapping, rename the source adapter
condition to a distinct type such as PolicyCheckPassed while keeping the
resource output and assertion as PolicyValid, so the mapping is verified.
- Around line 19-20: Gate TestConditionMapping_BEFORE with the inverse of
HYPERFLEET_TEST_CONDITION_MAPPING, matching the existing conditional gating in
TestConditionMapping_AFTER. Keep its QuotaValid-absent assertions unchanged so
the mutually exclusive tests run only against their corresponding
configurations.

---

Nitpick comments:
In `@pkg/config/conditions_test.go`:
- Around line 188-208: Extend the condition validation tests alongside the
existing invalid output status case to cover malformed CEL expressions in
output.reason and output.message. Add separate configurations using incomplete
expressions for each field, and assert wantError is true with the existing
“invalid CEL expression” error expectation, exercising the validateCELExpression
calls for both fields.

In `@pkg/config/conditions.go`:
- Around line 84-106: Deduplicate the repeated validation logic in the cluster
and nodepool sections of the configuration validation flow. Extract a helper
that accepts the entity name and condition-mapping collection, sorts its keys,
and calls validateConditionMapping for each entry while propagating errors;
invoke it for both c.Clusters and c.NodePools.

In `@pkg/services/condition_mapper_test.go`:
- Around line 1261-1392: Move the three Apply-focused subtests out of
TestTruncateUTF8 into appropriately named dedicated test functions, including
TestConditionMapper_ConcurrentApply for the concurrency case, so truncation
filters remain scoped correctly. In the concurrent test, remove the unused
result.err field and its assertion, and send/receive only
[]api.ResourceCondition through the channel while preserving the existing result
assertions.

In `@pkg/services/condition_mapper.go`:
- Around line 205-209: Update the error handling around validateFieldLengths in
NewConditionMapper’s condition-mapping flow: remove the redundant condition-type
length validation there, relying on ConditionsConfig.Validate() for validated
startup configuration. If validation can still fail for unvalidated
configurations, propagate or return the error instead of silently returning nil,
nil and dropping the condition.

In `@pkg/services/resource_test.go`:
- Around line 3160-3232: Extend
TestResourceService_ConditionMapper_IntegrationPath with a false-evaluating when
expression and assert no CustomReady condition is emitted. Add collision
coverage using mapped types Reconciled and ValidationSuccessful, asserting the
intended behavior from the condition-mapping logic without unintended overwrites
or duplicates. Reuse the existing test helpers and verify both outcomes through
rcDao.conditions.

In `@pkg/util/cel_test.go`:
- Around line 88-115: Extract the repeated CEL parse, check, program creation,
and evaluation sequence from the affected tests into a shared test helper marked
with t.Helper(). Have the helper accept the expression and resource data, return
the evaluated output and error, and update all three tests—including the block
around the additional referenced lines—to use it while preserving their existing
assertions and CEL cost limit.
- Around line 199-227: Extend TestToJsonFunc with error-path cases for the
toJSONFunc 1MB payload guard and CELCostLimit rejection. Feed toJson a
sufficiently large map and assert evaluation returns an error containing
“exceeds 1MB limit”; separately exercise a program that exceeds CELCostLimit and
assert the evaluation surfaces the cost-limit error.

In `@pkg/util/cel.go`:
- Around line 55-70: Update toJSONFunc so serialization enforces the 1MB limit
while writing, rather than calling json.Marshal and checking the fully allocated
result afterward. Use a size-capped JSON encoder or equivalent bounded writer,
preserve the existing toJson error behavior, and retain the maxJSONSize limit
without claiming it prevents allocations it cannot prevent.

In `@pkg/util/naming_test.go`:
- Around line 10-22: Extend the testCases table in the naming tests with
degenerate adapter inputs "", "-adapter", and "multi--word", asserting the
intended naming behavior for each. Use the existing naming function under test
and ensure these cases verify that empty or dash-edged names do not produce
unintended reserved condition types consumed by buildReservedConditionTypes.
🪄 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: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: ba0e7a9e-f99d-4e9b-b7fd-cd75c3691fa3

📥 Commits

Reviewing files that changed from the base of the PR and between 1ae18cb and 5dbbdef.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum, !**/go.sum
📒 Files selected for processing (20)
  • configs/config.yaml.example
  • go.mod
  • pkg/config/conditions.go
  • pkg/config/conditions_test.go
  • pkg/config/config.go
  • pkg/config/loader.go
  • pkg/services/aggregation.go
  • pkg/services/aggregation_test.go
  • pkg/services/condition_mapper.go
  • pkg/services/condition_mapper_test.go
  • pkg/services/resource.go
  • pkg/services/resource_test.go
  • pkg/util/cel.go
  • pkg/util/cel_test.go
  • pkg/util/mask_sensitive.go
  • pkg/util/mask_sensitive_test.go
  • pkg/util/naming.go
  • pkg/util/naming_test.go
  • plugins/resources/plugin.go
  • test/integration/condition_mapping_test.go
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • openshift-hyperfleet/architecture (manual)
  • openshift-hyperfleet/hyperfleet-api (manual)
  • openshift-hyperfleet/hyperfleet-sentinel (manual)
  • openshift-hyperfleet/hyperfleet-adapter (manual)
  • openshift-hyperfleet/hyperfleet-broker (manual)
💤 Files with no reviewable changes (1)
  • pkg/services/aggregation_test.go

Comment thread configs/config.yaml.example Outdated
Comment thread configs/config.yaml.example Outdated
Comment thread go.mod Outdated
Comment thread pkg/services/condition_mapper_test.go
Comment thread pkg/services/resource.go Outdated
Comment thread test/integration/condition_mapping_test.go
Comment thread test/integration/condition_mapping_test.go Outdated

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/services/condition_mapper_test.go (1)

1364-1366: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove the tautological error assertion.

result.err is never populated, and mapper.Apply returns only conditions. Therefore Expect(res.err).NotTo(HaveOccurred()) always passes and provides no error-path coverage. Remove the field/assertion or capture a real error from an error-returning API.

As per path instructions, error paths SHOULD be tested, not just represented by a never-populated error field.

Also applies to: 1387-1389, 1393-1396

🤖 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/services/condition_mapper_test.go` around lines 1364 - 1366, Remove the
unused result.err field and all tautological
Expect(res.err).NotTo(HaveOccurred()) assertions in the mapper.Apply tests,
including the referenced cases. Keep assertions focused on the conditions
returned by Apply; only test errors if an actual error-returning API is
introduced.

Source: Path instructions

🧹 Nitpick comments (1)
pkg/services/condition_mapper_test.go (1)

1403-1413: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mark testBuildActivation as a test helper. Pass *testing.T through, call t.Helper() immediately, and update the two call sites at pkg/services/condition_mapper_test.go:580 and :889 so failures point at the calling test.

🤖 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/services/condition_mapper_test.go` around lines 1403 - 1413, Update
testBuildActivation to accept *testing.T, call t.Helper() as its first
operation, and adjust both call sites around the tests at lines 580 and 889 to
pass their testing instance through. Preserve the existing activation-building
behavior while ensuring failures are attributed to the calling tests.

Source: Path instructions

🤖 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.

Outside diff comments:
In `@pkg/services/condition_mapper_test.go`:
- Around line 1364-1366: Remove the unused result.err field and all tautological
Expect(res.err).NotTo(HaveOccurred()) assertions in the mapper.Apply tests,
including the referenced cases. Keep assertions focused on the conditions
returned by Apply; only test errors if an actual error-returning API is
introduced.

---

Nitpick comments:
In `@pkg/services/condition_mapper_test.go`:
- Around line 1403-1413: Update testBuildActivation to accept *testing.T, call
t.Helper() as its first operation, and adjust both call sites around the tests
at lines 580 and 889 to pass their testing instance through. Preserve the
existing activation-building behavior while ensuring failures are attributed to
the calling tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: d45a63ca-d0b1-4251-8063-d27d97047dba

📥 Commits

Reviewing files that changed from the base of the PR and between d5c9913 and 767608e.

📒 Files selected for processing (2)
  • pkg/services/condition_mapper.go
  • pkg/services/condition_mapper_test.go
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • openshift-hyperfleet/architecture (manual)
  • openshift-hyperfleet/hyperfleet-api (manual)
  • openshift-hyperfleet/hyperfleet-sentinel (manual)
  • openshift-hyperfleet/hyperfleet-adapter (manual)
  • openshift-hyperfleet/hyperfleet-broker (manual)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/services/condition_mapper.go

Comment thread configs/config.yaml.example Outdated
conditions:
clusters:
# Example: Expose Landing Zone namespace readiness
# LandingZoneReady:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Viper lowercases map keys, so LandingZoneReady in YAML becomes landingzoneready internally

This means that the mapping names become lowercase.

Here is an output from one of my tests (see the last condition)

  {
     ...
      "kind": "Cluster",
      "name": "my-cluster4",
     ...
      "status": {
        "conditions": [
          {
             ...
            "status": "False",
            "type": "Reconciled"
          },
          {
            ...
            "status": "False",
            "type": "LastKnownReconciled"
          },
          {
            "created_time": "2026-07-28T10:22:02.861305Z",
            "last_transition_time": "2026-07-28T10:22:02.861305Z",
            "last_updated_time": "2026-07-28T10:22:02.861305Z",
            "message": "Landing zone: YYYYYYYYY YYYYYY",
            "observed_generation": 2,
            "reason": "XXXXXXXXX",
            "status": "True",
            "type": "landingzoneready"
          }
        ]
      },
   

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch! Updated to follow the same pattern as adapter conditions in hyperfleet-infra — array with explicit type: field preserves PascalCase.

Comment thread configs/config.yaml.example Outdated
# - reason: 256 bytes (truncated if exceeded, preserving UTF-8 boundaries)
# - message: 2048 bytes (truncated if exceeded, preserving UTF-8 boundaries)
conditions:
clusters:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If these are configured per entity type... what about having the conditions as part of the entity configuration?

entities:
  - kind: Cluster
    plural: clusters
    spec_schema_name: ClusterSpec
    required_adapters: [validation, dns, pullsecret, hypershift]
    conditions:
       -->  HERE  <--

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Migrated to entities[].conditions

Comment thread pkg/services/condition_mapper.go Outdated
// Log error but don't fail the entire aggregation
logger.With(ctx, "resource_kind", m.resourceKind, "condition_type", rule.conditionType).
WithError(err).
Warn("Failed to evaluate condition mapping rule, skipping")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Blocking

Category: Architecture

The design doc (condition-mapping-design.md § Error Handling) says:

If a CEL expression fails, the entire mapping operation fails and the database transaction is rolled back. [...] Accepting partial mapping results would prevent timely retry.

But Apply() logs a warning and skips the failing rule — partial results are committed. If this is intentional (e.g., you decided skip-and-continue is better for availability), the design doc should be updated to match. Otherwise, Apply should return an error that propagates up to recomputeAndSaveResourceConditions and triggers rollback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

ldornele and others added 11 commits July 30, 2026 11:16
Address code review findings from PR review:
- Move condition validation from pkg/config to pkg/registry (better cohesion)
- Add blank lines between adjacent top-level function declarations
- Update config.yaml.example documentation for Unknown filtering behavior
- Format code with gofmt

Changes:
- pkg/registry/conditions.go: Moved from pkg/config (validation logic belongs with registry)
- pkg/registry/conditions_test.go: Moved from pkg/config
- configs/config.yaml.example: Clarify Unknown filtering (entire adapter status dropped)
- pkg/services/condition_mapper_test.go: Add blank lines between functions
- pkg/util/cel_test.go: Add blank lines between functions
- pkg/util/mask_sensitive_test.go: Add blank line before TestIsSensitiveKey

All tests passing. No functional changes.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Design doc (condition-mapping-design.md § Error Handling) mandates:
"If a CEL expression fails, the entire mapping operation fails and
the database transaction is rolled back."

Changes:
- Apply() signature: ([]api.ResourceCondition, error) instead of []api.ResourceCondition
- CEL evaluation errors return error instead of skip-and-continue
- resource.go propagates error as GeneralError → triggers rollback
- Test: TestProcessAdapterStatus_ConditionMapperError_TriggersRollback

Impact: CEL failures trigger 10s retry instead of 30min delay.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@ldornele

Copy link
Copy Markdown
Contributor Author

/retest

Without MarkForRollback, transaction commits despite error.
Aligns with other error paths (lines 234, 265, 339, 932).
ldornele added 5 commits July 30, 2026 14:41
Expect(err) inside goroutine calls t.Fatal from non-test goroutine,
which panics. Move assertion to main test goroutine instead.

Fixes: send err through channel, assert on res.err after receive.
…ent skip

validateFieldLengths error returned (nil, nil) - indistinguishable
from when=false skip. Propagate error for consistency with CEL
rollback-on-failure design (lines 170-177).

Note: conditionType length validated at startup, so this is
defense-in-depth (runtime path unreachable in practice).
When hasUnknown=true, adapterStatusToMapWithUnknownCheck allocated a
full 4-key map that buildStatusesList immediately discarded. Return
nil instead - caller guards with if !hasUnknown so never reads the map.

Also adds test coverage for Unknown filtering path (missing coverage
for hasUnknown=true code path).
# Generic resource types registered at startup. Each entry auto-generates
# REST endpoints, spec validation, and delete policies.
#
# Condition Mapping (HYPERFLEET-538):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suggest to remove jira task references across this file - "HYPERFLEET-538", to not tie config with Jira

Comment on lines +108 to 142
#
# Condition Mapping (HYPERFLEET-538):
# Each entity can define CEL-based condition mapping rules that expose
# provider-specific adapter conditions in the public status.conditions array.
#
# Rules are compiled at startup (fail-fast). Invalid CEL expressions prevent API startup.
# Evaluation happens during status aggregation. Unknown adapter conditions are filtered.
#
# Reserved condition types (cannot be overridden by mapping):
# - Reconciled
# - LastKnownReconciled
# - Per-adapter synthesized types (auto-generated from required_adapters):
# Example: "validation" adapter → "ValidationSuccessful" condition type
#
# CEL Context Variables:
# - statuses: array of adapter statuses (adapter entries with any Unknown condition are excluded entirely)
# Each status: adapter (string), observed_generation (number), conditions (array), data (map)
# - resource: full cluster/nodepool object as map (sensitive fields masked)
# - env: environment variables as map (currently always empty, future enhancement)
#
# Custom CEL Functions:
# - toJson(value): marshal to JSON string
# - dig(target, "dot.path"): safe nested navigation
#
# Security: Adapter data fields matching sensitive patterns (password, secret, token,
# auth, private, connection, cert, credential, etc.) are automatically masked with
# "***REDACTED***" before CEL evaluation. This prevents credential leakage in public
# condition messages/reasons. See pkg/util/mask_sensitive.go for the full pattern list.
#
# Field Length Constraints:
# - type: 128 bytes (validation error if exceeded, prevents startup)
# - reason: 256 bytes (truncated if exceeded)
# - message: 2048 bytes (truncated if exceeded)
#
entities:

@mliptak0 mliptak0 Aug 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would decouple this block somewhere into /docs and instead, eg. docs/config.md, and keep only a link here or keep just the configuration example to keep it minimal

Comment thread pkg/config/loader.go
if valErr := config.Metrics.Validate(); valErr != nil {
return fmt.Errorf("metrics config validation failed: %w", valErr)
}
// Conditions validation now happens in registry.Validate() after entity descriptors are loaded

@mliptak0 mliptak0 Aug 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need this comment? (can be easily forgotten if anything changes which could make this statement not true)

Comment thread pkg/services/resource.go
Comment on lines +43 to +60
// Create condition mappers from entity descriptors
// Iterate all registered entities and build mappers from inline conditions
conditionMappers := make(map[string]*ConditionMapper)
for _, descriptor := range registry.All() {
if len(descriptor.Conditions) > 0 {
mapper, err := NewConditionMapper(descriptor.Kind, descriptor.Conditions)
if err != nil {
// This should not happen since config was validated at startup - indicates a code bug
// (e.g., validation logic vs. mapper logic mismatch). Use Error level to alert ops team,
// but continue in degraded mode (no CEL mapping) to keep service available per HYG-02.
logger.With(context.Background(), "resource_kind", descriptor.Kind).
WithError(err).
Error("Failed to create condition mapper, continuing without CEL mapping")
} else {
conditionMappers[descriptor.Kind] = mapper
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Small nit — this loop pulls straight from registry.All(), which is a bit inconsistent with the rest of the constructor since everything else (resourceDao, resourceLabelDao, etc.) is passed in explicitly. It also means the tests have to registry.Register(...) + t.Cleanup(registry.Reset) just to get a mapper in there, which is more than it needs to be.

Wonder if it'd be cleaner to pull this out into a buildConditionMappers(entities []registry.EntityDescriptor) helper next to NewConditionMapper in condition_mapper.go, and just call buildConditionMappers(registry.All()) here. Would keep the mapper-building logic where the rest of the mapper code lives, and make it easier to unit test without touching the global registry.

Comment thread pkg/util/naming.go
"unicode"
)

// adapterConditionSuffix is the suffix appended to adapter names when generating condition types (QUAL-01)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What does this stand for "QUAL-01"?

@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: 2

🧹 Nitpick comments (3)
pkg/services/resource_test.go (1)

3280-3301: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

The comment claims "standard conditions + mapped condition"; only the mapped one is asserted.

recomputeAndSaveResourceConditions appends mapped conditions to reconciled, lastKnownReconciled, and the per-adapter conditions. A regression that replaces instead of appends would still pass this test. Assert that the reserved types survive. findCondition already exists at Line 2357.

💚 Proposed addition
 	Expect(customReady).ToNot(BeNil(), "CustomReady condition should be created by mapper")
+	Expect(findCondition(conditions, "Reconciled")).ToNot(BeNil(),
+		"mapped conditions must be appended to, not replace, aggregated conditions")
+	Expect(findCondition(conditions, "LastKnownReconciled")).ToNot(BeNil(),
+		"LastKnownReconciled must survive condition mapping")
 	Expect(customReady.Status).To(Equal(api.ConditionTrue))

As per path instructions: "New exported functions and critical logic paths SHOULD have tests".

🤖 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/services/resource_test.go` around lines 3280 - 3301, Strengthen the test
around the condition collection in the mapped-condition verification by using
the existing findCondition helper to assert that the reserved standard condition
types remain present alongside CustomReady. Keep the existing mapped-condition
assertions, and verify the expected standard conditions are not replaced when
mapped conditions are appended.

Source: Path instructions

pkg/services/condition_mapper.go (1)

254-262: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The log message says "skipping"; the code rolls back the transaction.

validateFieldLengths returns an error here. evaluateRule propagates it, Apply propagates it, and recomputeAndSaveResourceConditions calls db.MarkForRollback. Nothing is skipped. An operator reading Condition type exceeds max length, skipping will not connect it to a failed adapter status report.

This branch is also unreachable in normal operation: registry.ValidateEntityConditions rejects a type longer than MaxConditionTypeLength at startup, so reaching it means the registry and the mapper disagree. Log at Error level and state the real effect, or drop the log and keep only the wrapped error.

♻️ Proposed fix
 	if len(rule.conditionType) > registry.MaxConditionTypeLength {
 		logger.With(
 			ctx,
 			"resource_kind", m.resourceKind,
 			"condition_type", rule.conditionType,
 			"length", len(rule.conditionType),
-		).Warn("Condition type exceeds max length, skipping")
-		return "", "", fmt.Errorf("condition type exceeds max length")
+		).Error("Condition type exceeds max length; failing mapping and rolling back")
+		return "", "", fmt.Errorf(
+			"condition type exceeds max length %d (got %d)",
+			registry.MaxConditionTypeLength, len(rule.conditionType),
+		)
 	}

As per path instructions: "Log levels must match severity".

🤖 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/services/condition_mapper.go` around lines 254 - 262, Update the
length-validation branch in validateFieldLengths to use Error-level logging and
describe that the condition type causes processing to fail and the transaction
to roll back, rather than saying it is skipped. Keep the existing error return
so evaluateRule, Apply, and recomputeAndSaveResourceConditions continue
propagating the failure.

Source: Path instructions

pkg/services/resource.go (1)

46-60: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Fail fast when NewConditionMapper violates validated registry invariants.

registry.Validate() runs before Environment().Initialize(), and both paths use the same CEL compilation pipeline and util.CELCostLimit. Any error indicates an initialization defect. Panic, or return an initialization error after changing the constructor signature, instead of omitting mapped conditions from status.conditions. Add a metric only if degraded mode remains intentional. (CWE-754)

🤖 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/services/resource.go` around lines 46 - 60, Update the initialization
flow around NewConditionMapper to fail fast when mapper creation returns an
error, rather than logging and continuing without storing the mapper. Propagate
an initialization error or panic consistently with the surrounding Environment
initialization contract, ensuring invalid registry invariants cannot silently
omit status conditions.
🤖 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 `@configs/config.yaml.example`:
- Around line 113-114: Update the configuration comment near the status
aggregation rules to state that adapters with unknown conditions are discarded
from statuses, matching parseConditionsWithUnknownCheck and buildStatusesList;
remove the claim that only individual unknown conditions are filtered.

In `@pkg/util/cel.go`:
- Around line 91-99: Update the encoder setup in toJSONFunc by removing the
SetEscapeHTML(false) override so encoding/json retains its default HTML escaping
for <, >, and &. Preserve the existing size-limit and error-handling behavior
around enc.Encode.

---

Nitpick comments:
In `@pkg/services/condition_mapper.go`:
- Around line 254-262: Update the length-validation branch in
validateFieldLengths to use Error-level logging and describe that the condition
type causes processing to fail and the transaction to roll back, rather than
saying it is skipped. Keep the existing error return so evaluateRule, Apply, and
recomputeAndSaveResourceConditions continue propagating the failure.

In `@pkg/services/resource_test.go`:
- Around line 3280-3301: Strengthen the test around the condition collection in
the mapped-condition verification by using the existing findCondition helper to
assert that the reserved standard condition types remain present alongside
CustomReady. Keep the existing mapped-condition assertions, and verify the
expected standard conditions are not replaced when mapped conditions are
appended.

In `@pkg/services/resource.go`:
- Around line 46-60: Update the initialization flow around NewConditionMapper to
fail fast when mapper creation returns an error, rather than logging and
continuing without storing the mapper. Propagate an initialization error or
panic consistently with the surrounding Environment initialization contract,
ensuring invalid registry invariants cannot silently omit status conditions.
🪄 Autofix

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: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: cdb97833-c783-4716-96d1-d53a05fff313

📥 Commits

Reviewing files that changed from the base of the PR and between d5c9913 and 909ecd9.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum, !**/go.sum
📒 Files selected for processing (20)
  • configs/config.yaml.example
  • go.mod
  • pkg/config/loader.go
  • pkg/registry/conditions.go
  • pkg/registry/conditions_test.go
  • pkg/registry/descriptor.go
  • pkg/registry/registry.go
  • pkg/services/aggregation.go
  • pkg/services/aggregation_test.go
  • pkg/services/condition_mapper.go
  • pkg/services/condition_mapper_test.go
  • pkg/services/resource.go
  • pkg/services/resource_test.go
  • pkg/util/cel.go
  • pkg/util/cel_test.go
  • pkg/util/mask_sensitive.go
  • pkg/util/mask_sensitive_test.go
  • pkg/util/naming.go
  • pkg/util/naming_test.go
  • test/integration/condition_mapping_test.go
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • openshift-hyperfleet/architecture (manual)
  • openshift-hyperfleet/hyperfleet-api (manual)
  • openshift-hyperfleet/hyperfleet-sentinel (manual)
  • openshift-hyperfleet/hyperfleet-adapter (manual)
  • openshift-hyperfleet/hyperfleet-broker (manual)
💤 Files with no reviewable changes (1)
  • pkg/services/aggregation_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • pkg/util/naming_test.go
  • pkg/util/naming.go
  • pkg/services/aggregation.go
  • pkg/util/mask_sensitive.go
  • test/integration/condition_mapping_test.go
  • pkg/util/mask_sensitive_test.go
  • go.mod

Comment on lines +113 to +114
# Rules are compiled at startup (fail-fast). Invalid CEL expressions prevent API startup.
# Evaluation happens during status aggregation. Unknown adapter conditions are filtered.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Line 114 contradicts line 123.

Line 114 states that unknown adapter conditions are filtered. The implementation in pkg/services/condition_mapper.go (parseConditionsWithUnknownCheck breaks and buildStatusesList discards the map) removes the whole adapter entry from statuses, not the single condition. Line 123 documents that correctly. An operator who reads only line 114 will write a when expression that never matches.

📝 Proposed fix
 # Rules are compiled at startup (fail-fast). Invalid CEL expressions prevent API startup.
-# Evaluation happens during status aggregation. Unknown adapter conditions are filtered.
+# Evaluation happens during status aggregation. An adapter reporting any Unknown
+# condition is excluded from `statuses` entirely (see CEL Context Variables below).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Rules are compiled at startup (fail-fast). Invalid CEL expressions prevent API startup.
# Evaluation happens during status aggregation. Unknown adapter conditions are filtered.
# Rules are compiled at startup (fail-fast). Invalid CEL expressions prevent API startup.
# Evaluation happens during status aggregation. An adapter reporting any Unknown
# condition is excluded from `statuses` entirely (see CEL Context Variables below).
🤖 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 `@configs/config.yaml.example` around lines 113 - 114, Update the configuration
comment near the status aggregation rules to state that adapters with unknown
conditions are discarded from statuses, matching parseConditionsWithUnknownCheck
and buildStatusesList; remove the claim that only individual unknown conditions
are filtered.

Comment thread pkg/util/cel.go
Comment on lines +91 to +99
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false) // Match json.Marshal behavior

if err := enc.Encode(v); err != nil {
if buf.exceeded {
return types.NewErr("toJson: output exceeds 1MB limit")
}
return types.NewErr("toJson: %v", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Go encoding/json does json.Marshal escape HTML by default and what does Encoder SetEscapeHTML(false) change

💡 Result:

Yes, Go's json.Marshal escapes specific HTML characters by default [1][2]. It automatically converts the characters "<", ">", "&", U+2028, and U+2029 into their Unicode-escaped forms (e.g., "<" becomes "\u003c") to ensure the generated JSON is safe to embed directly into HTML <script> tags without causing security issues [1][3][4]. The Encoder.SetEscapeHTML(false) method changes this behavior by disabling this automatic escaping [1][5]. When this is set to false, characters like "<", ">", and "&" remain as their literal selves in the output rather than being converted to Unicode escape sequences [2]. This is useful for scenarios where the JSON output is not intended for HTML embedding and readability is preferred [1][4]. Because json.Marshal does not expose an option to disable this behavior, you must use an Encoder if you wish to turn off HTML escaping [6][2]. Example usage: var buf bytes.Buffer enc:= json.NewEncoder(&buf) enc.SetEscapeHTML(false) enc.Encode(v) // Output will contain literal <, >, & characters [2]

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- pkg/util/cel.go ---'
cat -n pkg/util/cel.go | sed -n '1,180p'

printf '%s\n' '--- toJson usages ---'
rg -n -C 3 'toJson|ToJson|toJSON' --glob '*.go' . || true

printf '%s\n' '--- condition message/reason construction ---'
rg -n -C 3 'conditions|Condition|message|reason|Message|Reason' --glob '*.go' pkg | head -n 400 || true

Repository: openshift-hyperfleet/hyperfleet-api

Length of output: 31609


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- relevant standards ---'
find . -path '*hyperfleet/standards/code-review*' -type f -maxdepth 8 -print 2>/dev/null | sort | head -n 100
rg -n -C 3 'SEC-0[1-3]|output encoding|CWE-116|HTML|condition' hyperfleet/standards/code-review . 2>/dev/null | head -n 300 || true

Repository: openshift-hyperfleet/architecture

Length of output: 29697


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- CEL tests ---'
cat -n pkg/util/cel_test.go | sed -n '190,275p'

printf '%s\n' '--- condition mapping and status aggregation symbols ---'
rg -n -C 4 'ConditionMapping|condition mapping|toJson\\(|message|reason|AdapterCondition|ClusterCondition|ResourceCondition' --glob '*.go' . | head -n 600

printf '%s\n' '--- API schemas and status documentation ---'
rg -n -C 3 'message|reason|conditions|Conditions' --glob '*.yaml' --glob '*.yml' --glob '*.json' --glob '*.md' . | head -n 500

Repository: openshift-hyperfleet/hyperfleet-api

Length of output: 39361


🏁 Script executed:

#!/bin/bash
set -eu

# Standalone standard-library probe. It does not import or execute repository code.
cat >/tmp/json_escape_probe.go <<'EOF'
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
)

func main() {
	value := map[string]string{"value": "<tag>&"}
	marshalResult, marshalErr := json.Marshal(value)
	if marshalErr != nil {
		panic(marshalErr)
	}

	var buf bytes.Buffer
	encoder := json.NewEncoder(&buf)
	encoder.SetEscapeHTML(false)
	if err := encoder.Encode(value); err != nil {
		panic(err)
	}

	fmt.Printf("json.Marshal: %s\n", marshalResult)
	fmt.Printf("Encoder.SetEscapeHTML(false): %s", bytes.TrimSuffix(buf.Bytes(), []byte{'\n'}))
}
EOF
go run /tmp/json_escape_probe.go
rm -f /tmp/json_escape_probe.go

Repository: openshift-hyperfleet/hyperfleet-api

Length of output: 269


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- condition mapper files ---'
fd -i 'condition' . --type f | sort

printf '%s\n' '--- condition mapping references ---'
rg -n -C 5 'ConditionMapping|condition mapping|compileExpression|CELVarResource|CELVarStatuses|toJson' --glob '*.go' . | head -n 800

printf '%s\n' '--- assignments to condition reason/message ---'
rg -n -C 6 'Reason:|Message:|\.Reason|\.Message|reason.*condition|message.*condition' --glob '*.go' pkg | head -n 800

printf '%s\n' '--- configuration examples containing toJson ---'
rg -n -C 8 'toJson|message:|reason:' --glob '*.yaml' --glob '*.yml' --glob '*.json' --glob '*.md' config deploy charts docs . 2>/dev/null | head -n 800 || true

Repository: openshift-hyperfleet/hyperfleet-api

Length of output: 50392


Restore HTML escaping in toJSONFunc. json.Marshal escapes <, >, and &; SetEscapeHTML(false) emits them literally. CEL mappings can place this output in public condition messages and reasons. Keep escaping enabled to prevent raw adapter-controlled markup from reaching consumers (CWE-116).

🤖 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/util/cel.go` around lines 91 - 99, Update the encoder setup in toJSONFunc
by removing the SetEscapeHTML(false) override so encoding/json retains its
default HTML escaping for <, >, and &. Preserve the existing size-limit and
error-handling behavior around enc.Encode.

References []ReferenceDescriptor `mapstructure:"references" json:"references,omitempty"`
// CEL-based condition mapping rules for this entity type
// Each rule maps adapter conditions to public API conditions
// See HYPERFLEET-538 for condition mapping design

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is this self-reference?

// EntityDescriptor defines everything specific to a HyperFleet entity type.
// Descriptors are loaded from the application config YAML at startup via LoadDescriptors.
//
//nolint:govet // fieldalignment: gofmt field ordering conflicts with memory alignment optimization

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why nolint instead of fix?

for _, name := range m.sortedNames {
rule := m.rules[name]

// Lookup previous condition for this type (O(1) instead of O(N))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suggest running an agent to trim some comments like this, just noise

// Build lookup map for previous conditions to avoid O(N×M) linear scans
// Lookup previous condition for this type (O(1) instead of O(N))

"resource_kind", m.resourceKind,
"condition_type", rule.conditionType,
"length", len(rule.conditionType),
).Warn("Condition type exceeds max length, skipping")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the message says skipping, but tracing this path, it doesn't look like "skip" - it returns an error on top


// Field length constraints
const (
MaxConditionTypeLength = 128

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

database has VARCHAR(100)

// Expected: adapter custom conditions do NOT appear in public status.conditions
// NOTE: This test will PASS when the API is configured WITHOUT CEL mapping.
func TestConditionMapping_BEFORE(t *testing.T) {
if os.Getenv("HYPERFLEET_TEST_CONDITION_MAPPING") != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this variable appears nowhere else in the repo, not in makefile or configs 🤔

// Core Masking Tests
// ============================================================================

func TestMaskSensitiveFields(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I got a lot of errors while running these with the race flag. Could you verify, please?

Comment thread go.mod
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
google.golang.org/grpc v1.82.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

go mod tidy bringing it back to indirect

Comment thread pkg/util/cel.go
return types.NullValue
}
current = next
case []interface{}:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[]interface{} != []map[string]interface{} in Go, slices are invariant (thanks Go)
dig() returns null on real data because of this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants