Skip to content

HYPERFLEET-1295 - feat: Conditional creation of resources#236

Open
mliptak0 wants to merge 1 commit into
openshift-hyperfleet:mainfrom
mliptak0:HYPERFLEET-1295
Open

HYPERFLEET-1295 - feat: Conditional creation of resources#236
mliptak0 wants to merge 1 commit into
openshift-hyperfleet:mainfrom
mliptak0:HYPERFLEET-1295

Conversation

@mliptak0

@mliptak0 mliptak0 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a conditional creation gate for resources, symmetric to the existing conditional deletion gate. A resource can now declare lifecycle.create.when with a CEL expression that must hold before it is created for the first time; once the resource exists, the gate is ignored and it's reconciled normally like any other resource. This lets adapters express "only create this if X" (a feature flag, a sibling resource's discovered state, an event field) without making the whole resources phase all-or-nothing, the same way lifecycle.delete.when already does for deletion.

HYPERFLEET-1295

Changes

  • Added lifecycle.create / LifecycleCreate config type, mirroring the shape of lifecycle.delete
  • Added a create.when check in executeResource: evaluated only when the resource is absent from pre-discovery; skips creation (operation skip) when it evaluates to false, is ignored entirely once the resource already exists
  • Skipped creations now set adapter.resourcesSkipped / adapter.skipReason, the same signal precondition-skips already populate, so post-action when gates don't need to special-case which phase produced the skip
  • Unified create and delete lifecycle validation into a single validateLifecycleConfig pass, and unified their CEL evaluation into one evaluateLifecycleWhen helper (previously delete-only)
  • Pre-discovery (preDiscoverAll) now also runs when only lifecycle.create is configured, not just lifecycle.delete, since create.when needs to know whether the resource already exists
  • Documented the new lifecycle.create block in the adapter authoring guide and the task config template

Test Plan

  • Unit tests added/updated
  • make test-all passes
  • make lint passes
  • Helm chart changes validated with make test-helm (if applicable)
  • Deployed to a development cluster and verified (if Helm/config changes)
  • E2E tests passed (if cross-component or major changes)

@openshift-ci openshift-ci Bot requested review from crizzo71 and ma-hill July 9, 2026 09:55
@openshift-ci

openshift-ci Bot commented Jul 9, 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 mbrudnoy 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

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@mliptak0, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

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

Review profile: CHILL

Plan: Enterprise

Run ID: bc95d047-5fed-47f8-838a-1673b6cb169e

📥 Commits

Reviewing files that changed from the base of the PR and between 3830a2d and 724ff6d.

📒 Files selected for processing (8)
  • configs/adapter-task-config-template.yaml
  • docs/adapter-authoring-guide.md
  • internal/configloader/constants.go
  • internal/configloader/types.go
  • internal/configloader/validator.go
  • internal/configloader/validator_test.go
  • internal/executor/resource_executor.go
  • internal/executor/resource_executor_test.go
📝 Walkthrough

Walkthrough

This PR adds lifecycle.create.when gating for resource creation. It introduces LifecycleCreate and FieldLifecycleCreate, validates create and delete lifecycle blocks separately, updates executor pre-discovery and resource execution to evaluate create gating before apply, and records skip metadata when creation is blocked. The adapter task template and authoring guide are updated, and new validator and executor tests cover create and delete lifecycle cases.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ExecuteAll as ResourceExecutor.ExecuteAll
  participant Discovery as pre-discovery
  participant Execute as executeResource
  participant CEL as evaluateLifecycleWhen
  participant Apply as ApplyResource

  ExecuteAll->>Discovery: pre-discover resources with create/delete lifecycle
  Discovery-->>ExecuteAll: execCtx.Resources populated
  ExecuteAll->>Execute: process resource
  Execute->>Execute: check execCtx.Resources for existing object
  alt absent and lifecycle.create.when set
    Execute->>CEL: evaluate create expression
    CEL-->>Execute: true/false or error
    alt false
      Execute->>Execute: mark skipped and set adapter skip metadata
    else true
      Execute->>Apply: apply resource
    end
  else present or no create.when
    Execute->>Apply: apply resource
  end
Loading
🚥 Pre-merge checks | ✅ 9 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
No Pii Or Sensitive Data In Logs ⚠️ Warning FAIL: resource_executor.go logs raw CEL expressions and wrapped errors, risking CWE-532 disclosure of user-supplied PII/sensitive literals. Redact the expression from logs/errors; log only kind/match status, and sanitize any error text before WithErrorField/NewExecutorError.
✅ Passed checks (9 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary change: conditional resource creation.
Description check ✅ Passed The description directly matches the changeset and explains the new lifecycle.create behavior.
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 No non-test, non-example Go log statements contain token/password/credential/secret strings; no CWE-532 exposure found.
No Hardcoded Secrets ✅ Passed No hardcoded secrets, embedded creds, or long base64 literals were added; only docs examples/placeholder URLs and non-secret test constants appeared. CWE-798/CWE-259 not triggered.
No Weak Cryptography ✅ Passed No crypto/md5, crypto/des, crypto/rc4, ECB, custom crypto, or secret comparisons were added; changes are lifecycle/CEL only. CWE-327/CWE-328 not implicated.
No Injection Vectors ✅ Passed Touched code adds lifecycle structs/validators and CEL gating only; no new exec.Command, template.HTML, SQL string concat, or unsafe yaml.Unmarshal paths were introduced.
No Privileged Containers ✅ Passed No changed manifest, Helm, or Dockerfile paths; touched files contain no privileged flags (CWE-250 / CWE-266 risk indicators).
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

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.

@hyperfleet-ci-bot

hyperfleet-ci-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

Risk Score: 2 — risk/medium

Signal Detail Points
PR size 654 lines (>500) +2
Sensitive paths none +0
Test coverage Tests cover changed packages +0

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.

🧹 Nitpick comments (2)
internal/configloader/validator.go (1)

596-659: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Validation logic is sound; consider extracting create/delete validation into helpers.

validateLifecycleConfig is now ~72 lines with two parallel validation blocks. Extracting validateCreateLifecycle and validateDeleteLifecycle would improve readability and keep the function under the 50-line guideline. Not blocking — the current structure is clear and well-commented.

🤖 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/configloader/validator.go` around lines 596 - 659,
`validateLifecycleConfig` has grown too large with two parallel create/delete
validation blocks, so extract them into focused helpers to improve readability
and keep the function under the size guideline. Move the existing create logic
into a helper like `validateCreateLifecycle`, and the delete logic into
`validateDeleteLifecycle`, passing the resource, index, and any shared state
needed. Keep the same validation behavior and error paths/messages by reusing
the existing `validateCELExpression`, `FieldLifecycleCreate`, and
`FieldLifecycleDelete`-based logic inside the new helpers.

Source: Path instructions

internal/executor/resource_executor_test.go (1)

650-674: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for lifecycle.create with nil when (always-create default).

newResourceWithLifecycleCreate always sets When to a non-nil LifecycleWhen. The executor handles nil When by defaulting shouldCreate=true (line 133-138), and the validator permits lifecycle.create with no when block. This is a valid config shape with no test coverage.

♻️ Suggested test addition
 func TestResourceExecutor_LifecycleCreate_NilWhen_DefaultsTrue_Applied(t *testing.T) {
 	// lifecycle.create configured with no `when` block → always create (shouldCreate defaults true).
 	notFoundErr := apierrors.NewNotFound(schema.GroupResource{Resource: "configmaps"}, "test-cm")
 	discovered := &unstructured.Unstructured{Object: map[string]interface{}{
 		"apiVersion": "v1",
 		"kind":       "ConfigMap",
 		"metadata":   map[string]interface{}{"name": "test-cm", "namespace": "default"},
 	}}
 	mock := &sequencedGetResourceMock{
 		MockK8sClient: k8sclient.NewMockK8sClient(),
 		results:       []*unstructured.Unstructured{nil, discovered},
 		errs:          []error{notFoundErr, nil},
 	}
 	mock.ApplyResourceResult = &transportclient.ApplyResult{
 		Operation: manifest.OperationCreate,
 		Reason:    "mock create",
 	}

 	re := newResourceExecutor(&ExecutorConfig{
 		TransportClient: mock,
 		Logger:          logger.NewTestLogger(),
 	})

 	resource := configloader.Resource{
 		Name:      "test-resource",
 		Transport: &configloader.TransportConfig{Client: "kubernetes"},
 		Manifest: map[string]interface{}{
 			"apiVersion": "v1",
 			"kind":       "ConfigMap",
 			"metadata":   map[string]interface{}{"name": "test-cm", "namespace": "default"},
 		},
 		Discovery: &configloader.DiscoveryConfig{Namespace: "default", ByName: "test-cm"},
 		Lifecycle: &configloader.ResourceLifecycle{
 			Create: &configloader.LifecycleCreate{}, // When is nil
 		},
 	}
 	execCtx := NewExecutionContext(context.Background(), nil, nil)

 	results, err := re.ExecuteAll(context.Background(), []configloader.Resource{resource}, execCtx)

 	require.NoError(t, err)
 	require.Len(t, results, 1)
 	assert.Equal(t, StatusSuccess, results[0].Status)
 	assert.Equal(t, manifest.OperationCreate, results[0].Operation)
 	assert.False(t, execCtx.Adapter.ResourcesSkipped)
 }
🤖 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/executor/resource_executor_test.go` around lines 650 - 674, Add
coverage for the `lifecycle.create` default path where `LifecycleCreate.When` is
nil. Introduce a test alongside the existing resource executor tests that builds
a resource with `newResourceWithLifecycleCreate`-style setup but omits the
`When` block entirely, then verify the executor still creates the resource by
taking the `shouldCreate=true` default in the `ResourceExecutor` create flow.
Keep the test aligned with the existing `configloader.Resource`,
`ResourceLifecycle`, and `LifecycleCreate` structures so it exercises the
validator-accepted nil-when shape.
🤖 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.

Nitpick comments:
In `@internal/configloader/validator.go`:
- Around line 596-659: `validateLifecycleConfig` has grown too large with two
parallel create/delete validation blocks, so extract them into focused helpers
to improve readability and keep the function under the size guideline. Move the
existing create logic into a helper like `validateCreateLifecycle`, and the
delete logic into `validateDeleteLifecycle`, passing the resource, index, and
any shared state needed. Keep the same validation behavior and error
paths/messages by reusing the existing `validateCELExpression`,
`FieldLifecycleCreate`, and `FieldLifecycleDelete`-based logic inside the new
helpers.

In `@internal/executor/resource_executor_test.go`:
- Around line 650-674: Add coverage for the `lifecycle.create` default path
where `LifecycleCreate.When` is nil. Introduce a test alongside the existing
resource executor tests that builds a resource with
`newResourceWithLifecycleCreate`-style setup but omits the `When` block
entirely, then verify the executor still creates the resource by taking the
`shouldCreate=true` default in the `ResourceExecutor` create flow. Keep the test
aligned with the existing `configloader.Resource`, `ResourceLifecycle`, and
`LifecycleCreate` structures so it exercises the validator-accepted nil-when
shape.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Enterprise

Run ID: 5ab21189-0ef4-4d70-943a-6625dde4e673

📥 Commits

Reviewing files that changed from the base of the PR and between 95da817 and c266e6c.

📒 Files selected for processing (8)
  • configs/adapter-task-config-template.yaml
  • docs/adapter-authoring-guide.md
  • internal/configloader/constants.go
  • internal/configloader/types.go
  • internal/configloader/validator.go
  • internal/configloader/validator_test.go
  • internal/executor/resource_executor.go
  • internal/executor/resource_executor_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)


// discovery is required — without it executeResource cannot determine whether
// the resource already exists, and will always evaluate the when condition.
switch {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking (Architecture) — Confidence: High

The architecture design doc (PR #182, Migration & Compatibility section) says:

Set without when → config validation rejects it (mirrors lifecycle.delete's requirement).

But the implementation accepts lifecycle.create without when (test at validator_test.go:1187 explicitly validates this). The JIRA says "optional when field," so the implementation seems correct — the design doc should be updated to match.

Please align the two PRs: either update the design doc to say when is optional, or add a validation case here to reject lifecycle.create without when.

Logger: logger.NewTestLogger(),
})

// Invalid CEL syntax — evaluateLifecycleCreateWhen will error.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit (Pattern) — Confidence: High

This comment references evaluateLifecycleCreateWhen — a function that doesn't exist. The PR unified the old evaluateLifecycleDeleteWhen into a shared evaluateLifecycleWhen (taking a kind parameter).

Suggested change
// Invalid CEL syntax — evaluateLifecycleCreateWhen will error.
// Invalid CEL syntax — evaluateLifecycleWhen will error.

Comment on lines +140 to +146
if whenErr != nil {
result.Status = StatusFailed
result.Error = whenErr
re.recordResourceError(execCtx, resource, whenErr)
return result, NewExecutorError(PhaseResources, resource.Name, "failed to evaluate lifecycle.create.when", whenErr)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit (Inconsistency) — Confidence: Medium

The create error path doesn't set result.Operation, but the analogous delete error path (line 177) sets result.Operation = manifest.OperationDelete. Since we're in the !resourceFound branch, the intended operation is creation.

Suggested change
if whenErr != nil {
result.Status = StatusFailed
result.Error = whenErr
re.recordResourceError(execCtx, resource, whenErr)
return result, NewExecutorError(PhaseResources, resource.Name, "failed to evaluate lifecycle.create.when", whenErr)
}
if whenErr != nil {
result.Status = StatusFailed
result.Operation = manifest.OperationCreate
result.Error = whenErr
re.recordResourceError(execCtx, resource, whenErr)
return result, NewExecutorError(PhaseResources, resource.Name, "failed to evaluate lifecycle.create.when", whenErr)
}

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.

2 participants