Skip to content

OSAC-1459: Extend provisioning lifecycle for multi-job tracking - #147

Merged
openshift-merge-bot[bot] merged 1 commit into
osac-project:mainfrom
SiddarthR56:OSAC-1459
Aug 5, 2026
Merged

openshift-merge-bot[bot] merged 1 commit into
osac-project:mainfrom
SiddarthR56:OSAC-1459

Conversation

@SiddarthR56

@SiddarthR56 SiddarthR56 commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added support for provisioning and deprovisioning resources through multiple targets.
    • Job statuses now identify the target handling each operation.
    • Added independent tracking, retries, polling, and error handling for each target.
    • Preserved existing behavior for single-target resources.
  • Validation

    • Added checks for missing, duplicate, or invalid target configurations.

@openshift-ci-robot

openshift-ci-robot commented Aug 5, 2026 •

Copy link
Copy Markdown

@SiddarthR56: This pull request references OSAC-1459 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the task to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change adds an optional target to job status records and CRD schemas. Provisioning and deprovisioning lifecycles now track jobs, retries, polling, validation, errors, and requeues independently for multiple targets.

Changes

Multi-target provisioning lifecycle

Layer / File(s) Summary
Target status contracts
osac-operator/api/v1alpha1/job_types.go, osac-operator/charts/operator-crds/templates/*, osac-operator/config/crd/bases/*
JobStatus and job-status CRD schemas now include an optional target string.
Target-scoped job tracking and backoff
osac-operator/pkg/provisioning/job_helpers.go, osac-operator/pkg/provisioning/provision_helpers.go, osac-operator/pkg/provisioning/provision_lifecycle.go
Job lookup, retry history, lifecycle evaluation, triggering, logging, and status updates now match the requested target. Existing unscoped APIs pass an empty target.
Multi-target provisioning orchestration
osac-operator/pkg/provisioning/provision_lifecycle.go, osac-operator/pkg/provisioning/provision_lifecycle_test.go
Provisioning validates target definitions, executes targets independently, aggregates errors, selects the earliest requeue, flushes status once, and tests target-specific behavior.
Multi-target deprovisioning orchestration
osac-operator/pkg/provisioning/provision_lifecycle.go, osac-operator/pkg/provisioning/provision_lifecycle_test.go
Deprovisioning scopes triggering, polling, backoff, status synchronization, completion, validation, and error aggregation by target.

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

Sequence Diagram(s)

sequenceDiagram
  participant Controller
  participant ProvisioningLifecycle
  participant TargetProvider
  participant ResourceStatus
  Controller->>ProvisioningLifecycle: run target list
  ProvisioningLifecycle->>TargetProvider: evaluate and process each target
  TargetProvider-->>ProvisioningLifecycle: job result and requeue
  ProvisioningLifecycle->>ResourceStatus: flush status after job creation
  ProvisioningLifecycle-->>Controller: earliest requeue and aggregated error
Loading
🚥 Pre-merge checks | ✅ 11
✅ Passed checks (11 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: extending the provisioning lifecycle to support multi-job tracking.
Docstring Coverage ✅ Passed Docstring coverage is 96.43% which is sufficient. The required threshold is 80.00%.
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.
No-Hardcoded-Secrets ✅ Passed The 25-file HEAD diff adds target fields, lifecycle code, and tests; scans found no sensitive assignments, credential URLs, private keys, vendor credential formats, or encoded secret literals.
No-Weak-Crypto ✅ Passed The PR adds target-scoped lifecycle tracking only; precise scans of all added lines found no weak algorithms, crypto APIs, custom crypto, or secret/token comparisons.
No-Injection-Vectors ✅ Passed Changed Go/YAML files only add target data and lifecycle logic; scans found no SQL concatenation, shell=True, eval/exec, pickle.loads, yaml.load, os.system, or dangerouslySetInnerHTML.
Container-Privileges ✅ Passed The PR changes only CRD schemas and Go code; all changed YAML objects are CustomResourceDefinition, and added-line scans found no privileged, host*, SYS_ADMIN, or allowPrivilegeEscalation settings.
No-Sensitive-Data-In-Logs ✅ Passed Changed logging adds only target, job ID, state, config version, and timing fields; no passwords, tokens, API keys, PII, or customer data are logged.
Ai-Attribution ✅ Passed The target commit names Cursor/Claude and includes Assisted-by: Cursor/Claude; it has no AI Co-Authored-By trailer.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (4)
osac-operator/pkg/provisioning/provision_lifecycle.go (2)

96-108: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Log the API server read failure instead of discarding it.

If apiReader.Get fails, the function returns false. The caller then treats the resource as having no in-flight job and triggers a new provision job. That is exactly the duplicate-job case this guard exists to prevent. Record the error so operators can see why the guard was bypassed.

♻️ Proposed fix
 	log := ctrllog.FromContext(ctx)
 	if err := apiReader.Get(ctx, key, fresh); err != nil {
+		log.Error(err, "failed to read resource from API server; proceeding without duplicate-job check", "target", target)
 		return false
 	}

As per path instructions "Never ignore error returns".

🤖 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 `@osac-operator/pkg/provisioning/provision_lifecycle.go` around lines 96 - 108,
Update CheckAPIServerForNonTerminalProvisionJobAndTarget to log the error
returned by apiReader.Get before returning false, including the resource key and
error details so operators can identify why the guard was bypassed. Preserve the
existing return behavior.

Source: Path instructions


229-275: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Put error last in the runLifecycleCore signature.

The function returns (ctrl.Result, error, bool). Go convention places error as the final return value, and revive's error-return rule flags this shape. The function is unexported and has two call sites, so the change is contained.

♻️ Proposed fix
-) (ctrl.Result, error, bool) {
+) (ctrl.Result, bool, error) {
 	action, latestJob := evaluateActionForTarget(provState, target, checkAPIServer)
@@
 	switch action {
 	case Skip:
-		return ctrl.Result{}, nil, false
+		return ctrl.Result{}, false, nil
 	case Trigger:
 		res, err := trigger()
-		return res, err, triggered
+		return res, triggered, err
 	case Requeue:
-		return ctrl.Result{RequeueAfter: pollInterval}, nil, false
+		return ctrl.Result{RequeueAfter: pollInterval}, false, nil
 	case Backoff:
 		res, err := handleBackoffForTarget(ctx, provState, target, latestJob, trigger)
-		return res, err, triggered
+		return res, triggered, err
 	default: // Poll
 		res, err := PollJob(ctx, provider, resource, provState, latestJob, pollInterval, callbacks)
-		return res, err, false
+		return res, false, nil
 	}

Update both call sites accordingly:

-	result, err, triggered := runLifecycleCore(ctx, provider, resource, provState, "", maxHistory, pollInterval, callbacks, checkAPIServer)
+	result, triggered, err := runLifecycleCore(ctx, provider, resource, provState, "", maxHistory, pollInterval, callbacks, checkAPIServer)
-		res, err, triggered := runLifecycleCore(ctx, t.Provider, resource, provState, t.Name, maxHistory, pollInterval, t.Callbacks, t.CheckAPIServer)
+		res, triggered, err := runLifecycleCore(ctx, t.Provider, resource, provState, t.Name, maxHistory, pollInterval, t.Callbacks, t.CheckAPIServer)

As per path instructions for Go files.

🤖 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 `@osac-operator/pkg/provisioning/provision_lifecycle.go` around lines 229 -
275, Change runLifecycleCore to return (ctrl.Result, bool, error), placing error
last, and update both call sites to unpack and return the values in the new
order. Preserve the existing result, triggered, and error semantics across every
action branch.

Source: Path instructions

osac-operator/pkg/provisioning/provision_lifecycle_test.go (2)

1201-1226: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The mock discards provisionJobs, so cross-target leakage cannot be asserted.

mockProvider.TriggerDeprovision drops its provisionJobs argument before calling triggerDeprovisionFunc. No test in this suite can therefore observe what the lifecycle passes to a provider. That is why the unscoped *jobs argument in triggerDeprovisionJobForTarget has no failing test.

Widen the mock's callback to accept provisionJobs, then assert that a target's provider only receives its own provision jobs.

♻️ Proposed fix
-	triggerDeprovisionFunc   func(ctx context.Context, resource client.Object) (*DeprovisionResult, error)
+	triggerDeprovisionFunc   func(ctx context.Context, resource client.Object, provisionJobs []v1alpha1.JobStatus) (*DeprovisionResult, error)
 func (m *mockProvider) TriggerDeprovision(ctx context.Context, resource client.Object, provisionJobs []v1alpha1.JobStatus) (*DeprovisionResult, error) {
-	return m.triggerDeprovisionFunc(ctx, resource)
+	return m.triggerDeprovisionFunc(ctx, resource, provisionJobs)
 }

Update every triggerDeprovisionFunc literal to the new signature, then add:

ginkgo.It("passes only the target's own provision jobs to that target's provider", func() {
	var fabricSaw []v1alpha1.JobStatus
	fabricProvider := &mockProvider{
		triggerDeprovisionFunc: func(_ context.Context, _ client.Object, pj []v1alpha1.JobStatus) (*DeprovisionResult, error) {
			fabricSaw = pj
			return &DeprovisionResult{Action: DeprovisionTriggered, JobID: "fabric-deprov"}, nil
		},
	}
	jobs := []v1alpha1.JobStatus{
		{JobID: "fabric-prov", Type: v1alpha1.JobTypeProvision, Target: "fabric", State: v1alpha1.JobStateRunning, Timestamp: metav1.NewTime(time.Now())},
		{JobID: "k8s-prov", Type: v1alpha1.JobTypeProvision, Target: "k8s", State: v1alpha1.JobStateRunning, Timestamp: metav1.NewTime(time.Now())},
	}
	targets := []DeprovisionTarget{{Name: "fabric", Provider: fabricProvider}}
	_, _, err := RunMultiTargetDeprovisioningLifecycle(ctx, targets, resource, &jobs, maxHistory, pollInterval)
	Expect(err).NotTo(HaveOccurred())
	for _, j := range fabricSaw {
		Expect(j.Target).To(Equal("fabric"))
	}
})
🤖 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 `@osac-operator/pkg/provisioning/provision_lifecycle_test.go` around lines 1201
- 1226, Update mockProvider’s triggerDeprovisionFunc callback signature and
every corresponding literal to receive provisionJobs, forwarding that argument
from TriggerDeprovision. Add a test around RunMultiTargetDeprovisioningLifecycle
that supplies provision jobs for multiple targets and verifies the provider for
one target receives only jobs whose Target matches that target.

281-306: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for pre-existing untagged job history.

This suite always starts from either an empty job list or jobs already tagged with Target. No test covers a resource that carries a legacy job with Target: "" while the multi-target lifecycle runs. That is the upgrade path for every resource provisioned before this change.

Once you decide the migration behavior for untagged history, encode it here. The test makes the chosen contract explicit and prevents a silent re-provision regression.

ginkgo.It("<states the decided behavior for a legacy untagged succeeded provision job>", func() {
	resource := &v1alpha1.Subnet{}
	jobs := []v1alpha1.JobStatus{
		{JobID: "legacy-1", Type: v1alpha1.JobTypeProvision, Target: "",
			State: v1alpha1.JobStateSucceeded, ConfigVersion: "v1",
			Timestamp: metav1.NewTime(time.Now())},
	}
	// ... assert whether "fabric" re-triggers or reuses the untagged job.
})

Do you want me to write this test once the migration behavior is settled?

🤖 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 `@osac-operator/pkg/provisioning/provision_lifecycle_test.go` around lines 281
- 306, Add a Ginkgo test alongside the existing
RunMultiTargetProvisioningLifecycle tests that seeds provState.Jobs with a
succeeded legacy provision JobStatus whose Target is empty, then runs the
multi-target lifecycle and asserts the explicitly chosen migration behavior for
both targets—whether the untagged history is reused or each target is
re-triggered. Include the existing config version and timestamp fields so the
test represents a real upgrade path.
🤖 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 `@osac-operator/pkg/provisioning/provision_lifecycle.go`:
- Around line 426-438: Filter jobs by matching Target before passing them from
triggerDeprovisionJobForTarget to TriggerDeprovision. In
osac-operator/pkg/provisioning/provision_lifecycle.go lines 426-438, pass only
the target’s provision jobs; in
osac-operator/pkg/provisioning/provision_lifecycle_test.go lines 1201-1226,
update mockProvider.triggerDeprovisionFunc to accept and forward provisionJobs,
then assert each provider receives only its own jobs.
- Around line 304-347: Choose and implement one migration strategy for legacy
jobs with Target == ""—backfill targets or treat untagged history as belonging
to the designated transition target—so existing resources are not reprovisioned
and deletions are not blocked. Document the chosen behavior in
RunMultiTargetProvisioningLifecycle at
osac-operator/pkg/provisioning/provision_lifecycle.go:304-347 and apply it in
RunMultiTargetDeprovisioningLifecycle at
osac-operator/pkg/provisioning/provision_lifecycle.go:593-639. Add coverage in
osac-operator/pkg/provisioning/provision_lifecycle_test.go:281-306 using
untagged jobs for both provisioning and deprovisioning, asserting the selected
behavior.

---

Nitpick comments:
In `@osac-operator/pkg/provisioning/provision_lifecycle_test.go`:
- Around line 1201-1226: Update mockProvider’s triggerDeprovisionFunc callback
signature and every corresponding literal to receive provisionJobs, forwarding
that argument from TriggerDeprovision. Add a test around
RunMultiTargetDeprovisioningLifecycle that supplies provision jobs for multiple
targets and verifies the provider for one target receives only jobs whose Target
matches that target.
- Around line 281-306: Add a Ginkgo test alongside the existing
RunMultiTargetProvisioningLifecycle tests that seeds provState.Jobs with a
succeeded legacy provision JobStatus whose Target is empty, then runs the
multi-target lifecycle and asserts the explicitly chosen migration behavior for
both targets—whether the untagged history is reused or each target is
re-triggered. Include the existing config version and timestamp fields so the
test represents a real upgrade path.

In `@osac-operator/pkg/provisioning/provision_lifecycle.go`:
- Around line 96-108: Update CheckAPIServerForNonTerminalProvisionJobAndTarget
to log the error returned by apiReader.Get before returning false, including the
resource key and error details so operators can identify why the guard was
bypassed. Preserve the existing return behavior.
- Around line 229-275: Change runLifecycleCore to return (ctrl.Result, bool,
error), placing error last, and update both call sites to unpack and return the
values in the new order. Preserve the existing result, triggered, and error
semantics across every action branch.
🪄 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: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 02cb27f9-7baa-4299-862e-918b3ca93a2e

📥 Commits

Reviewing files that changed from the base of the PR and between 45b4742 and 8bd3363.

📒 Files selected for processing (25)
  • osac-operator/api/v1alpha1/job_types.go
  • osac-operator/charts/operator-crds/templates/osac.openshift.io_clusterorders.yaml
  • osac-operator/charts/operator-crds/templates/osac.openshift.io_computeinstances.yaml
  • osac-operator/charts/operator-crds/templates/osac.openshift.io_externalipattachments.yaml
  • osac-operator/charts/operator-crds/templates/osac.openshift.io_externalippools.yaml
  • osac-operator/charts/operator-crds/templates/osac.openshift.io_externalips.yaml
  • osac-operator/charts/operator-crds/templates/osac.openshift.io_natgateways.yaml
  • osac-operator/charts/operator-crds/templates/osac.openshift.io_securitygroups.yaml
  • osac-operator/charts/operator-crds/templates/osac.openshift.io_subnets.yaml
  • osac-operator/charts/operator-crds/templates/osac.openshift.io_tenants.yaml
  • osac-operator/charts/operator-crds/templates/osac.openshift.io_virtualnetworks.yaml
  • osac-operator/config/crd/bases/osac.openshift.io_clusterorders.yaml
  • osac-operator/config/crd/bases/osac.openshift.io_computeinstances.yaml
  • osac-operator/config/crd/bases/osac.openshift.io_externalipattachments.yaml
  • osac-operator/config/crd/bases/osac.openshift.io_externalippools.yaml
  • osac-operator/config/crd/bases/osac.openshift.io_externalips.yaml
  • osac-operator/config/crd/bases/osac.openshift.io_natgateways.yaml
  • osac-operator/config/crd/bases/osac.openshift.io_securitygroups.yaml
  • osac-operator/config/crd/bases/osac.openshift.io_subnets.yaml
  • osac-operator/config/crd/bases/osac.openshift.io_tenants.yaml
  • osac-operator/config/crd/bases/osac.openshift.io_virtualnetworks.yaml
  • osac-operator/pkg/provisioning/job_helpers.go
  • osac-operator/pkg/provisioning/provision_helpers.go
  • osac-operator/pkg/provisioning/provision_lifecycle.go
  • osac-operator/pkg/provisioning/provision_lifecycle_test.go

Comment on lines +304 to +347
// RunMultiTargetProvisioningLifecycle runs the same evaluate/trigger/poll/
// backoff state machine as RunProvisioningLifecycle independently for each
// target, against job history tagged by JobStatus.Target. Requeues at the
// soonest interval any target still needs (ctrl.Result{} only once every
// target has reached Skip), and joins errors from all targets rather than
// short-circuiting on the first one — one target's failure never prevents
// another target's attempt in the same call. statusFlush is called at most
// once per call, only if at least one target triggered a new job.
//
// Every target's manager must have actually been dispatched to before being
// included here — a target that was never dispatched (e.g. a resource with
// no k8s manager configured) has no job history to evaluate and should be
// omitted by the caller rather than passed in.
func RunMultiTargetProvisioningLifecycle(
ctx context.Context,
targets []JobTarget,
resource client.Object,
provState *State,
maxHistory int,
pollInterval time.Duration,
statusFlush func() error,
) (ctrl.Result, error) {
if err := validateJobTargets(targets); err != nil {
return ctrl.Result{}, err
}

var (
errs []error
anyTriggered bool
result ctrl.Result
)

for _, t := range targets {
res, err, triggered := runLifecycleCore(ctx, t.Provider, resource, provState, t.Name, maxHistory, pollInterval, t.Callbacks, t.CheckAPIServer)
if err != nil {
errs = append(errs, fmt.Errorf("target %q: %w", t.Name, err))
}
if triggered {
anyTriggered = true
}
if res.RequeueAfter > 0 && (result.RequeueAfter == 0 || res.RequeueAfter < result.RequeueAfter) {
result.RequeueAfter = res.RequeueAfter
}
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Pre-change job history is unreachable from every multi-target path. FindLatestJobByTypeAndTarget matches Target on exact string equality, and both validators reject an empty Name. Every job persisted before this change carries Target == "", so no multi-target lookup can ever find it. One migration decision — backfill Target on existing records, or treat untagged history as belonging to a designated target during a transition window — resolves all three sites.

  • osac-operator/pkg/provisioning/provision_lifecycle.go#L304-L347: document the chosen migration behavior in the RunMultiTargetProvisioningLifecycle doc comment, next to the existing "never dispatched" note, and implement it so an already-provisioned resource is not re-provisioned on upgrade.
  • osac-operator/pkg/provisioning/provision_lifecycle.go#L593-L639: apply the same behavior in RunMultiTargetDeprovisioningLifecycle. Here an unmatched legacy deprovision job triggers a second teardown, done stays false, and the finalizer is never removed, so the CR blocks deletion.
  • osac-operator/pkg/provisioning/provision_lifecycle_test.go#L281-L306: add a test that starts from a job with Target: "" and asserts the decided behavior, for both the provisioning and the deprovisioning suite.
📍 Affects 2 files
  • osac-operator/pkg/provisioning/provision_lifecycle.go#L304-L347 (this comment)
  • osac-operator/pkg/provisioning/provision_lifecycle.go#L593-L639
  • osac-operator/pkg/provisioning/provision_lifecycle_test.go#L281-L306
🤖 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 `@osac-operator/pkg/provisioning/provision_lifecycle.go` around lines 304 -
347, Choose and implement one migration strategy for legacy jobs with Target ==
""—backfill targets or treat untagged history as belonging to the designated
transition target—so existing resources are not reprovisioned and deletions are
not blocked. Document the chosen behavior in RunMultiTargetProvisioningLifecycle
at osac-operator/pkg/provisioning/provision_lifecycle.go:304-347 and apply it in
RunMultiTargetDeprovisioningLifecycle at
osac-operator/pkg/provisioning/provision_lifecycle.go:593-639. Add coverage in
osac-operator/pkg/provisioning/provision_lifecycle_test.go:281-306 using
untagged jobs for both provisioning and deprovisioning, asserting the selected
behavior.

Comment on lines +426 to 438
func triggerDeprovisionJobForTarget(ctx context.Context, provider ProvisioningProvider, resource client.Object,
jobs *[]v1alpha1.JobStatus, target string, maxHistory int, pollInterval time.Duration) (ctrl.Result, error) {
log := ctrllog.FromContext(ctx)
log.Info("triggering deprovision job")
log.Info("triggering deprovision job", "target", target)

result, err := provider.TriggerDeprovision(ctx, resource, *jobs)
if err != nil {
if rateLimitErr, ok := AsRateLimitError(err); ok {
log.Info("deprovision request rate-limited, requeueing", "retryAfter", rateLimitErr.RetryAfter)
log.Info("deprovision request rate-limited, requeueing", "target", target, "retryAfter", rateLimitErr.RetryAfter)
return ctrl.Result{RequeueAfter: rateLimitErr.RetryAfter}, nil
}
return ctrl.Result{}, fmt.Errorf("failed to trigger deprovision: %w", err)
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

TriggerDeprovision still receives every target's provision jobs. triggerDeprovisionJobForTarget passes *jobs unfiltered, while the ProvisioningProvider contract says the provider uses that slice to find and cancel a running provision job. One target's provider can therefore cancel another target's job. The test mock hides this because it drops the argument.

  • osac-operator/pkg/provisioning/provision_lifecycle.go#L426-L438: filter the slice to provision jobs whose Target equals target before passing it to provider.TriggerDeprovision.
  • osac-operator/pkg/provisioning/provision_lifecycle_test.go#L1201-L1226: widen mockProvider.triggerDeprovisionFunc to accept provisionJobs, forward the real argument, and assert that each target's provider receives only its own provision jobs.
📍 Affects 2 files
  • osac-operator/pkg/provisioning/provision_lifecycle.go#L426-L438 (this comment)
  • osac-operator/pkg/provisioning/provision_lifecycle_test.go#L1201-L1226
🤖 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 `@osac-operator/pkg/provisioning/provision_lifecycle.go` around lines 426 -
438, Filter jobs by matching Target before passing them from
triggerDeprovisionJobForTarget to TriggerDeprovision. In
osac-operator/pkg/provisioning/provision_lifecycle.go lines 426-438, pass only
the target’s provision jobs; in
osac-operator/pkg/provisioning/provision_lifecycle_test.go lines 1201-1226,
update mockProvider.triggerDeprovisionFunc to accept and forward provisionJobs,
then assert each provider receives only its own jobs.

@ori-amizur ori-amizur left a comment

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.

Four issues flagged — two I consider blocking (#1 and #3), two are cleanup (#2 and #4). Details in inline comments.

log.Info("triggering deprovision job")
log.Info("triggering deprovision job", "target", target)

result, err := provider.TriggerDeprovision(ctx, resource, *jobs)

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.

Bug: unfiltered jobs passed to provider.

This passes the entire *jobs slice (all targets) to provider.TriggerDeprovision. The ProvisioningProvider.TriggerDeprovision interface takes provisionJobs []v1alpha1.JobStatus so providers can inspect provision job history (e.g., to cancel a running provision before deprovisioning). A fabric provider receiving k8s jobs — or vice versa — would see irrelevant history and could make wrong decisions based on it.

Suggestion: filter *jobs to only those matching target before passing:

targetJobs := filterJobsByTarget(*jobs, target)
result, err := provider.TriggerDeprovision(ctx, resource, targetJobs)

The test mock currently discards the provisionJobs argument in triggerDeprovisionFunc, so this bug has no failing test. Consider widening the mock callback signature to accept and assert on provisionJobs.

callbacks *PollCallbacks,
checkAPIServer func() bool,
) (ctrl.Result, error, bool) {
action, latestJob := evaluateActionForTarget(provState, target, checkAPIServer)

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.

Nit: error should be the last return value.

runLifecycleCore returns (ctrl.Result, error, bool). Go convention (and revive's error-return rule) places error last. Since this function is unexported with only two call sites, swapping to (ctrl.Result, bool, error) is a contained change.

// included here — a target that was never dispatched (e.g. a resource with
// no k8s manager configured) has no job history to evaluate and should be
// omitted by the caller rather than passed in.
func RunMultiTargetProvisioningLifecycle(

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.

Missing migration path for legacy untagged jobs.

Existing resources have jobs with Target: "". When a controller switches from RunProvisioningLifecycle to RunMultiTargetProvisioningLifecycle, those untagged jobs won't match any non-empty target name — FindLatestJobByTypeAndTarget(jobs, Provision, "fabric") returns nil for a job with Target: "". Each target will see no prior job history and trigger a fresh provision, potentially re-provisioning something that's already running.

The test suite starts from either empty jobs or already-tagged jobs; there's no test for the upgrade path. I'd suggest at minimum:

  1. Document the expected migration behavior (e.g., does the caller need to backfill Target on existing jobs before switching to multi-target? Or should evaluateActionForTarget treat untagged jobs as belonging to a designated legacy target?).
  2. Add a test with pre-existing Target: "" jobs to encode whichever strategy you choose.

func CheckAPIServerForNonTerminalProvisionJobAndTarget(ctx context.Context, apiReader client.Reader, key client.ObjectKey, fresh client.Object, extract JobsExtractor, target string) bool {
log := ctrllog.FromContext(ctx)
if err := apiReader.Get(ctx, key, fresh); err != nil {
return false

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.

Silenced API server read error.

When apiReader.Get fails, this returns false (no non-terminal job found), which bypasses the duplicate-job guard and lets a new job be triggered. The error is silently discarded. Consider logging it so operators can diagnose why the guard was bypassed:

if err := apiReader.Get(ctx, key, fresh); err != nil {
	log.Error(err, "failed to read resource from API server; proceeding without duplicate-job check", "target", target)
	return false
}

@openshift-ci

openshift-ci Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: danmanor, SiddarthR56

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

The pull request process is described 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 added the approved label Aug 5, 2026
@openshift-merge-bot
openshift-merge-bot Bot merged commit 464aed5 into osac-project:main Aug 5, 2026
42 checks passed
SiddarthR56 added a commit to SiddarthR56/osac that referenced this pull request Aug 10, 2026
Fixes findings from CodeRabbit and ori-amizur on PRs osac-project#225 and osac-project#147:

- resolveImplementationStrategy now falls back to the k8s target for
  K8sFallback kinds instead of the legacy strategy.
- SecurityGroupReconciler errors on an ambiguous parent VirtualNetwork
  lookup instead of silently picking one.
- Subnet clears its stale k8s implementation-strategy annotation when a
  NetworkClass transitions from dual-dispatch back to fabric-only, and
  handleDeprovisioning guards against an empty fabricStrategy.
- cmd/main.go reuses a single NetworkClassesClient instead of
  constructing a duplicate.
- TriggerDeprovision now only receives the calling target's own
  provision jobs instead of every target's history.
- Multi-target provisioning/deprovisioning backfills pre-existing
  untagged (Target == "") job history onto a designated target
  (JobTarget/DeprovisionTarget.AbsorbsLegacyHistory) so resources
  provisioned before dual-dispatch existed aren't re-provisioned or
  re-deprovisioned on upgrade.
- runLifecycleCore now returns (Result, bool, error), matching Go's
  error-last convention.
- CheckAPIServerForNonTerminalProvisionJobAndTarget logs API server
  read failures instead of silently bypassing the duplicate-job guard.

Assisted-by: Cursor/Claude
Co-authored-by: Cursor <cursoragent@cursor.com>
SiddarthR56 added a commit to SiddarthR56/osac that referenced this pull request Aug 12, 2026
Fixes findings from CodeRabbit and ori-amizur on PRs osac-project#225 and osac-project#147:

- resolveImplementationStrategy now falls back to the k8s target for
  K8sFallback kinds instead of the legacy strategy.
- SecurityGroupReconciler errors on an ambiguous parent VirtualNetwork
  lookup instead of silently picking one.
- Subnet clears its stale k8s implementation-strategy annotation when a
  NetworkClass transitions from dual-dispatch back to fabric-only, and
  handleDeprovisioning guards against an empty fabricStrategy.
- cmd/main.go reuses a single NetworkClassesClient instead of
  constructing a duplicate.
- TriggerDeprovision now only receives the calling target's own
  provision jobs instead of every target's history.
- Multi-target provisioning/deprovisioning backfills pre-existing
  untagged (Target == "") job history onto a designated target
  (JobTarget/DeprovisionTarget.AbsorbsLegacyHistory) so resources
  provisioned before dual-dispatch existed aren't re-provisioned or
  re-deprovisioned on upgrade.
- runLifecycleCore now returns (Result, bool, error), matching Go's
  error-last convention.
- CheckAPIServerForNonTerminalProvisionJobAndTarget logs API server
  read failures instead of silently bypassing the duplicate-job guard.

Assisted-by: Cursor/Claude
Co-authored-by: Cursor <cursoragent@cursor.com>
SiddarthR56 added a commit to SiddarthR56/osac that referenced this pull request Aug 13, 2026
Fixes findings from CodeRabbit and ori-amizur on PRs osac-project#225 and osac-project#147:

- resolveImplementationStrategy now falls back to the k8s target for
  K8sFallback kinds instead of the legacy strategy.
- SecurityGroupReconciler errors on an ambiguous parent VirtualNetwork
  lookup instead of silently picking one.
- Subnet clears its stale k8s implementation-strategy annotation when a
  NetworkClass transitions from dual-dispatch back to fabric-only, and
  handleDeprovisioning guards against an empty fabricStrategy.
- cmd/main.go reuses a single NetworkClassesClient instead of
  constructing a duplicate.
- TriggerDeprovision now only receives the calling target's own
  provision jobs instead of every target's history.
- Multi-target provisioning/deprovisioning backfills pre-existing
  untagged (Target == "") job history onto a designated target
  (JobTarget/DeprovisionTarget.AbsorbsLegacyHistory) so resources
  provisioned before dual-dispatch existed aren't re-provisioned or
  re-deprovisioned on upgrade.
- runLifecycleCore now returns (Result, bool, error), matching Go's
  error-last convention.
- CheckAPIServerForNonTerminalProvisionJobAndTarget logs API server
  read failures instead of silently bypassing the duplicate-job guard.

Assisted-by: Cursor/Claude
Co-authored-by: Cursor <cursoragent@cursor.com>
SiddarthR56 added a commit to SiddarthR56/osac that referenced this pull request Aug 13, 2026
Fixes findings from CodeRabbit and ori-amizur on PRs osac-project#225 and osac-project#147:

- resolveImplementationStrategy now falls back to the k8s target for
  K8sFallback kinds instead of the legacy strategy.
- SecurityGroupReconciler errors on an ambiguous parent VirtualNetwork
  lookup instead of silently picking one.
- Subnet clears its stale k8s implementation-strategy annotation when a
  NetworkClass transitions from dual-dispatch back to fabric-only, and
  handleDeprovisioning guards against an empty fabricStrategy.
- cmd/main.go reuses a single NetworkClassesClient instead of
  constructing a duplicate.
- TriggerDeprovision now only receives the calling target's own
  provision jobs instead of every target's history.
- Multi-target provisioning/deprovisioning backfills pre-existing
  untagged (Target == "") job history onto a designated target
  (JobTarget/DeprovisionTarget.AbsorbsLegacyHistory) so resources
  provisioned before dual-dispatch existed aren't re-provisioned or
  re-deprovisioned on upgrade.
- runLifecycleCore now returns (Result, bool, error), matching Go's
  error-last convention.
- CheckAPIServerForNonTerminalProvisionJobAndTarget logs API server
  read failures instead of silently bypassing the duplicate-job guard.

Assisted-by: Cursor/Claude
Co-authored-by: Cursor <cursoragent@cursor.com>
SiddarthR56 added a commit to SiddarthR56/osac that referenced this pull request Aug 17, 2026
Fixes findings from CodeRabbit and ori-amizur on PRs osac-project#225 and osac-project#147:

- resolveImplementationStrategy now falls back to the k8s target for
  K8sFallback kinds instead of the legacy strategy.
- SecurityGroupReconciler errors on an ambiguous parent VirtualNetwork
  lookup instead of silently picking one.
- Subnet clears its stale k8s implementation-strategy annotation when a
  NetworkClass transitions from dual-dispatch back to fabric-only, and
  handleDeprovisioning guards against an empty fabricStrategy.
- cmd/main.go reuses a single NetworkClassesClient instead of
  constructing a duplicate.
- TriggerDeprovision now only receives the calling target's own
  provision jobs instead of every target's history.
- Multi-target provisioning/deprovisioning backfills pre-existing
  untagged (Target == "") job history onto a designated target
  (JobTarget/DeprovisionTarget.AbsorbsLegacyHistory) so resources
  provisioned before dual-dispatch existed aren't re-provisioned or
  re-deprovisioned on upgrade.
- runLifecycleCore now returns (Result, bool, error), matching Go's
  error-last convention.
- CheckAPIServerForNonTerminalProvisionJobAndTarget logs API server
  read failures instead of silently bypassing the duplicate-job guard.

Assisted-by: Cursor/Claude
Co-authored-by: Cursor <cursoragent@cursor.com>
SiddarthR56 added a commit to SiddarthR56/osac that referenced this pull request Aug 18, 2026
Fixes findings from CodeRabbit and ori-amizur on PRs osac-project#225 and osac-project#147:

- resolveImplementationStrategy now falls back to the k8s target for
  K8sFallback kinds instead of the legacy strategy.
- SecurityGroupReconciler errors on an ambiguous parent VirtualNetwork
  lookup instead of silently picking one.
- Subnet clears its stale k8s implementation-strategy annotation when a
  NetworkClass transitions from dual-dispatch back to fabric-only, and
  handleDeprovisioning guards against an empty fabricStrategy.
- cmd/main.go reuses a single NetworkClassesClient instead of
  constructing a duplicate.
- TriggerDeprovision now only receives the calling target's own
  provision jobs instead of every target's history.
- Multi-target provisioning/deprovisioning backfills pre-existing
  untagged (Target == "") job history onto a designated target
  (JobTarget/DeprovisionTarget.AbsorbsLegacyHistory) so resources
  provisioned before dual-dispatch existed aren't re-provisioned or
  re-deprovisioned on upgrade.
- runLifecycleCore now returns (Result, bool, error), matching Go's
  error-last convention.
- CheckAPIServerForNonTerminalProvisionJobAndTarget logs API server
  read failures instead of silently bypassing the duplicate-job guard.

Assisted-by: Cursor/Claude
Co-authored-by: Cursor <cursoragent@cursor.com>
amito pushed a commit to amito/osac that referenced this pull request Aug 19, 2026
Fixes findings from CodeRabbit and ori-amizur on PRs osac-project#225 and osac-project#147:

- resolveImplementationStrategy now falls back to the k8s target for
  K8sFallback kinds instead of the legacy strategy.
- SecurityGroupReconciler errors on an ambiguous parent VirtualNetwork
  lookup instead of silently picking one.
- Subnet clears its stale k8s implementation-strategy annotation when a
  NetworkClass transitions from dual-dispatch back to fabric-only, and
  handleDeprovisioning guards against an empty fabricStrategy.
- cmd/main.go reuses a single NetworkClassesClient instead of
  constructing a duplicate.
- TriggerDeprovision now only receives the calling target's own
  provision jobs instead of every target's history.
- Multi-target provisioning/deprovisioning backfills pre-existing
  untagged (Target == "") job history onto a designated target
  (JobTarget/DeprovisionTarget.AbsorbsLegacyHistory) so resources
  provisioned before dual-dispatch existed aren't re-provisioned or
  re-deprovisioned on upgrade.
- runLifecycleCore now returns (Result, bool, error), matching Go's
  error-last convention.
- CheckAPIServerForNonTerminalProvisionJobAndTarget logs API server
  read failures instead of silently bypassing the duplicate-job guard.

Assisted-by: Cursor/Claude
Co-authored-by: Cursor <cursoragent@cursor.com>
eliorerz pushed a commit that referenced this pull request Sep 18, 2026
…ext keys, render version load error

Per rawagner review on PR #147:
- Remove trim() on cluster version id (useClusterVersion) and on
  versionName in the create payload; both come from trusted sources
  (backend id, dropdown selection) that never contain whitespace.
- Rename the version query destructure to isLoading/error and render the
  actual error message via getErrorMessage in the load-error Alert.
- Replace dotted i18n keys with the English-text keys used across the
  codebase (Select a version, Could not load cluster versions, Retry,
  This version is deprecated ...).

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Elay Aharoni <elayaha@gmail.com>
eliorerz pushed a commit that referenced this pull request Sep 18, 2026
…rsion-wizard

OSAC-2169: [UI] Implement version selection in cluster creation wizard

This branch was previously deployed

1 inactive deployment
e2e-test — 8bd33639 Deployed Aug 5, 2026 by SiddarthR56 via e2e-bmaas-full-install / e2e #513
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