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

OSAC-23: Extract storage lifecycle into OSAC Storage Controller - #299

Merged
openshift-merge-bot[bot] merged 15 commits into
osac-project:mainfrom
zszabo-rh:feat/OSAC-23-storage-controller
Jun 23, 2026
Merged

openshift-merge-bot[bot] merged 15 commits into
osac-project:mainfrom
zszabo-rh:feat/OSAC-23-storage-controller

Conversation

@zszabo-rh

@zszabo-rh zszabo-rh commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Extracts storage provisioning and deprovisioning logic from the Tenant controller into a dedicated OSAC Storage Controller, as specified in the design document (merged) and PRD (merged).

Key changes:

  • New StorageReconciler watches Tenant CRs as a secondary controller with its own finalizer (osac.openshift.io/storage)
  • Owns two conditions: StorageBackendReady (Stage 1: hub Secret) and ClusterStorageReady (Stage 2: StorageClasses on target cluster)
  • Two independent AAP provider instances: BackendProvider and ClusterStorageProvider with separate template env vars
  • New status types: StorageBackendStatus and ClusterStorageStatus for future multi-backend/multi-cluster support
  • Tenant controller simplified to namespace + UDN management only (zero storage logic)
  • New controller enable flag: OSAC_ENABLE_STORAGE_CONTROLLER / --enable-storage-controller
  • AAP client fix: uses numeric template ID when available (resolves launch-by-name issues with non-default AAP projects)

Env vars:

OSAC_STORAGE_BACKEND_AAP_PROVISION_TEMPLATE    (default: osac-create-tenant-storage-backend)
OSAC_STORAGE_BACKEND_AAP_DEPROVISION_TEMPLATE  (default: osac-delete-tenant-storage-backend)
OSAC_STORAGE_CLUSTER_AAP_PROVISION_TEMPLATE    (default: osac-create-tenant-cluster-storage)
OSAC_STORAGE_CLUSTER_AAP_DEPROVISION_TEMPLATE  (default: osac-delete-tenant-cluster-storage)

Dependencies:

  • AAP playbook split: osac-aap PR #338 must be merged before enabling the storage controller (playbook filenames must match template names)
  • Merge order: This PR first (operator), then AAP PR [AI] OSAC-1827: Operator treats transient KubeVirt PrintableStatus as fatal provisioning failure #338. This is safe because the storage controller is disabled by default (OSAC_ENABLE_STORAGE_CONTROLLER=false) — it won't attempt to launch any templates until explicitly enabled. Merging AAP first would be risky: the config-as-code sync would rename the existing templates, breaking the current tenant controller on any deployment that has it enabled.

Test plan

  • 427/428 unit tests pass (1 pre-existing flaky race condition)
  • E2E tested on beaker SNO (edge-17) with VAST appliance — 16 tests across 5 phases (June 12), 6 re-tests after design alignment (June 15)
  • Full tenant lifecycle: create → backend provision → cluster storage → delete
  • Management-state Unmanaged: skips reconciliation, deletion still runs
  • Playbook idempotency verified for all 4 lifecycle actions
  • CRD upgrade path: existing tenants auto-reconcile with new condition names

Assisted-by: Claude Code noreply@anthropic.com

Summary by CodeRabbit

  • New Features

    • Added a dedicated Storage controller with conditional enablement and storage-specific AAP template overrides.
    • Enhanced Tenant status reporting with separate “Backend Ready” and “Cluster Storage” readiness signals and dedicated backend/cluster storage job histories.
  • Breaking Changes

    • Standardized provisioning job history naming across resources from status.jobs to status.provisioningJobs.
    • Updated Tenant readiness/reasoning by splitting storage readiness into backend vs cluster storage conditions.
  • Bug Fixes / Improvements

    • Improved readiness/job status isolation between backend and cluster storage flows.
    • Added storage secrets read-only access for the operator and reordered CRD schemas for clearer status layout.

@openshift-ci-robot

openshift-ci-robot commented Jun 17, 2026

Copy link
Copy Markdown

@zszabo-rh: This pull request references OSAC-23 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 epic to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Summary

Extracts storage provisioning and deprovisioning logic from the Tenant controller into a dedicated OSAC Storage Controller, as specified in the design document (merged) and PRD (merged).

Key changes:

  • New StorageReconciler watches Tenant CRs as a secondary controller with its own finalizer (osac.openshift.io/storage)
  • Owns two conditions: StorageBackendReady (Stage 1: hub Secret) and ClusterStorageReady (Stage 2: StorageClasses on target cluster)
  • Two independent AAP provider instances: BackendProvider and ClusterStorageProvider with separate template env vars
  • New status types: StorageBackendStatus and ClusterStorageStatus for future multi-backend/multi-cluster support
  • Tenant controller simplified to namespace + UDN management only (zero storage logic)
  • New controller enable flag: OSAC_ENABLE_STORAGE_CONTROLLER / --enable-storage-controller
  • AAP client fix: uses numeric template ID when available (resolves launch-by-name issues with non-default AAP projects)

Env vars:

OSAC_STORAGE_BACKEND_AAP_PROVISION_TEMPLATE    (default: osac-create-tenant-storage-backend)
OSAC_STORAGE_BACKEND_AAP_DEPROVISION_TEMPLATE  (default: osac-delete-tenant-storage-backend)
OSAC_STORAGE_CLUSTER_AAP_PROVISION_TEMPLATE    (default: osac-create-tenant-cluster-storage)
OSAC_STORAGE_CLUSTER_AAP_DEPROVISION_TEMPLATE  (default: osac-delete-tenant-cluster-storage)

Dependencies:

Test plan

  • 427/428 unit tests pass (1 pre-existing flaky race condition)
  • E2E tested on beaker SNO (edge-17) with VAST appliance — 16 tests across 5 phases (June 12), 6 re-tests after design alignment (June 15)
  • Full tenant lifecycle: create → backend provision → cluster storage → delete
  • Management-state Unmanaged: skips reconciliation, deletion still runs
  • Playbook idempotency verified for all 4 lifecycle actions
  • CRD upgrade path: existing tenants auto-reconcile with new condition names

Assisted-by: Claude Code noreply@anthropic.com

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.

@openshift-ci
openshift-ci Bot requested review from jhernand and rgolangh June 17, 2026 12:40
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Extracts storage provisioning from TenantReconciler into a new StorageReconciler managing backend and cluster-storage job lifecycles with separate job arrays and conditions. Renames Jobs to ProvisioningJobs across all status types, adds StorageBackendReady/ClusterStorageReady conditions and new status structs, adds AAP TemplateID routing for template lookups, updates provisioning lifecycle to pass job slices to providers, and propagates schema/RBAC changes across all CRDs and controllers.

Changes

Storage Controller Extraction and Tenant API Expansion

Layer / File(s) Summary
Tenant API: new conditions, status structs, and job field renames
api/v1alpha1/tenant_types.go, api/v1alpha1/clusterorder_types.go, api/v1alpha1/computeinstance_types.go, api/v1alpha1/publicip_types.go, api/v1alpha1/publicipattachment_types.go, api/v1alpha1/publicippool_types.go, api/v1alpha1/securitygroup_types.go, api/v1alpha1/subnet_types.go, api/v1alpha1/virtualnetwork_types.go, api/v1alpha1/zz_generated.deepcopy.go
Replaces TenantConditionStorageClassReady with StorageBackendReady and ClusterStorageReady; adds TenantReasonNoProvider; introduces StorageBackendStatus (name/provider/ready/message) and ClusterStorageStatus (clusterName/ready/reason) exported structs; extends TenantStatus with storageBackends, clusterStorage, storageBackendJobs, clusterStorageJobs fields; renames Jobs→ProvisioningJobs in all resource status types; removes Tenant.GetStatusJobs(); regenerates deepcopy implementations.
AAP client TemplateID routing and provider deprovision signature
pkg/aap/client.go, pkg/provisioning/aap_provider.go
Adds TemplateID int to LaunchJobTemplateRequest and LaunchWorkflowTemplateRequest; updates LaunchJobTemplate/LaunchWorkflowTemplate to build URLs from TemplateID when >0, falling back to TemplateName; updates AAPProvider.TriggerDeprovision to accept current provisioning jobs slice and pass to isReadyForDeprovision for extracting latest provision job.
Tenant identifier constants and predicate helpers
internal/controller/tenant_names.go
Replaces osacTenantAnnotation with osacTenantKey label constant; removes eventReasonStorageClassNotReady; adds tenantNamespacePredicate, tenantLabelSelector, and storageClassTenantPredicate helpers; adds imports for metav1/client/predicate.
Storage tier resolution helper module
internal/controller/storage_tier_resolution.go
New file implementing getTenantStorageClasses with tierResolutionResult accumulator, groupByTier (lowercase/validated label grouping), per-tier cardinality resolution (single match wins, multiple → duplicate error, fallback to shared default), and conditionMessage concatenation.
TenantReconciler refactored to namespace-only concerns
internal/controller/tenant_controller.go, internal/controller/tenant_controller_test.go
Removes ProvisioningProvider/StatusPollInterval/MaxJobHistory from struct/constructor; rewrites handleUpdate to add finalizer, reset status, check namespace existence, set NamespaceReady condition, advance phase to Ready; simplifies handleDelete to phase + finalizer only; removes storage-class watch wiring; tests reduced to namespace-existence assertions.
StorageReconciler controller implementation
internal/controller/storage_controller.go
New ~730-line controller: Reconcile skips Unmanaged and gates on phase=Ready; handleUpdate orchestrates storage finalizer, hub Secret check, StorageBackendReady condition, StorageClass resolution with duplicate warnings, backend/cluster provisioning jobs; handleDelete enforces ordered class-then-backend deprovisioning with BlockDeletionOnFailure; watch setup covers Tenants, StorageClasses, Secrets, ClusterOrders.
StorageReconciler comprehensive test coverage
internal/controller/storage_controller_test.go
Adds ~670-line Ginkgo suite: skip when Tenant not Ready, trigger backend provisioning when hub Secret missing, StorageBackendReady=True on Secret, NoProvider reason, provider error recording, cluster provisioning trigger, ClusterStorageReady=True with discovered classes, tier cardinality/fallback, finalizer lifecycle, Unmanaged skip, job array isolation, cluster failure recording, condition transitions, deprovisioning ordering, duplicate detection.
Provisioning lifecycle: JobsExtractor and deprovision signature updates
pkg/provisioning/provision_lifecycle.go, pkg/provisioning/provider.go, pkg/provisioning/provision_lifecycle_test.go
Introduces JobsExtractor function type for typed job extraction; removes GetJobsFromResource switch; updates CheckAPIServerForNonTerminalProvisionJob to accept JobsExtractor; updates ProvisioningProvider.TriggerDeprovision interface to accept provisionJobs slice; updates TriggerDeprovisionJob to pass current jobs to provider.
cmd/main.go: Storage controller wiring and Tenant simplification
cmd/main.go
Adds OSAC_ENABLE_STORAGE_CONTROLLER flag and Storage entry in controllerFlags; adds env vars for AAP storage template overrides; simplifies setupTenantController to 3-arg form; adds setupStorageController creating two AAP direct providers and wiring StorageReconciler; updates main() to conditionally start storage controller.
ComputeInstance: osacTenantKey migration and ClusterStorageReady adoption
internal/controller/computeinstance_controller.go, internal/controller/computeinstance_resources.go, internal/controller/computeinstance_controller_test.go, internal/controller/computeinstance_integration_test.go, internal/controller/computeinstance_provisioning_test.go
Field index switched to osacTenantKey; tenant-not-ready diagnostic references TenantConditionClusterStorageReady; all job tracking uses ProvisioningJobs; deprovisionging refactored to RunDeprovisioningLifecycle; all unit, provisioning, and integration tests updated.
All remaining controllers: ProvisioningJobs field migration
internal/controller/clusterorder_controller.go, internal/controller/publicip_controller.go, internal/controller/publicipattachment_controller.go, internal/controller/publicippool_controller.go, internal/controller/securitygroup_controller.go, internal/controller/subnet_controller.go, internal/controller/virtualnetwork_controller.go, internal/controller/*_test.go, pkg/provisioning/*_test.go
All controllers switch Status.Jobs→Status.ProvisioningJobs; provisioning lifecycle wiring updated; API-server non-terminal job checks return ProvisioningJobs; deprovisioning lifecycle receivers updated; all controller and provisioning tests updated.
CRD schema updates and RBAC for storage secrets
config/crd/bases/*, charts/operator-crds/templates/*, config/rbac/kustomization.yaml, config/rbac/storage_secrets_role.yaml, config/rbac/storage_secrets_role_binding.yaml, .gitignore
All CRDs rename status.jobs→status.provisioningJobs; Tenant CRD adds clusterStorage, clusterStorageJobs, storageBackendJobs, storageBackends arrays; updates printer columns; adds storage-secrets-reader Role/RoleBinding; adds .artifacts/ to .gitignore.

Sequence Diagram

sequenceDiagram
  participant Operator as Operator (main)
  participant TenantReconciler as TenantReconciler
  participant StorageReconciler as StorageReconciler
  participant BackendProvider as AAP BackendProvider
  participant ClusterStorageProvider as AAP ClusterStorageProvider
  participant TenantStatus as Tenant CR Status

  Operator->>TenantReconciler: reconcile Tenant
  TenantReconciler->>TenantStatus: ensure finalizer, check namespace
  TenantReconciler->>TenantStatus: set NamespaceReady=True, phase=Ready

  Operator->>StorageReconciler: reconcile Tenant (phase=Ready)
  StorageReconciler->>StorageReconciler: ensure storage finalizer
  StorageReconciler->>StorageReconciler: hubSecretExists?
  alt hub Secret missing
    StorageReconciler->>BackendProvider: TriggerProvision
    BackendProvider-->>StorageReconciler: jobID
    StorageReconciler->>TenantStatus: append StorageBackendJobs, StorageBackendReady=False
  else hub Secret found
    StorageReconciler->>TenantStatus: set StorageBackendReady=True
    StorageReconciler->>StorageReconciler: getTenantStorageClasses
    StorageReconciler->>ClusterStorageProvider: TriggerProvision
    ClusterStorageProvider-->>StorageReconciler: jobID
    StorageReconciler->>TenantStatus: append ClusterStorageJobs, ClusterStorageReady=True
  end
  StorageReconciler->>TenantStatus: patch if changed
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • osac-project/osac-operator#199: Implements per-tier StorageClass resolution and TenantConditionStorageClassReady in tenant_controller.go, which this PR replaces with StorageBackendReady/ClusterStorageReady conditions and moves resolution logic to the new Storage controller.
  • osac-project/osac-operator#155: Adds duplicate StorageClass detection and condition-based events in Tenant controller, both superseded by this PR's refactoring and new condition types.
  • osac-project/osac-operator#158: Changes shared provisioning lifecycle APIs (TriggerDeprovision signature, job extraction wiring) that this PR directly updates.

Suggested reviewers

  • akshaynadkarni
  • danmanor
  • eranco74

Poem

Two controllers where one once stood,
Jobs renamed, as they rightly should.
Storage backend, cluster tiers split — 🗂️
ProvisioningJobs now perfectly fit.
The tenant slims down, namespace its beat,
While StorageReconciler takes the storage seat. 🏗️

🚥 Pre-merge checks | ✅ 10 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and concisely summarizes the primary change: extracting storage lifecycle logic into a dedicated OSAC Storage Controller, which is the main architectural shift across the PR's 40+ modified/new files.
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 No hardcoded secrets, API keys, tokens, passwords, or credentials found. All sensitive values are properly read from environment variables and passed as parameters. Test tokens are clearly marked a...
No-Weak-Crypto ✅ Passed No weak cryptographic algorithms (MD5, SHA1, DES, RC4, 3DES, Blowfish, ECB), custom crypto implementations, or non-constant-time secret comparisons detected. Only standard crypto/tls and crypto/x50...
No-Injection-Vectors ✅ Passed No injection vectors found. Code uses safe patterns: template IDs from JSON unmarshaling, operator-controlled env vars for template names, proper use of fmt.Sprintf for URL construction, and Go's s...
Container-Privileges ✅ Passed No container privilege escalation markers found in K8s manifests. Deployment enforces allowPrivilegeEscalation: false, runAsNonRoot: true, and DROP ALL capabilities.
No-Sensitive-Data-In-Logs ✅ Passed No sensitive data exposed in logs. Authentication tokens (aapToken) are never logged. AAP server URL is logged appropriately for operational debugging without embedded credentials.
Ai-Attribution ✅ Passed PR correctly attributes AI tool usage with "Assisted-by: Claude Code" trailer in proper format; no Co-Authored-By misuse detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
internal/controller/tenant_controller.go (1)

154-170: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Keep the finalizer until target namespace and UDN cleanup completes.

The delete path removes tenantFinalizer immediately, but this controller still owns tenant namespace/UDN lifecycle. That can orphan target-cluster isolation resources after the Tenant CR disappears. Delete or confirm removal of the target Namespace and UserDefinedNetwork before dropping the finalizer.

As per coding guidelines, Tenant — namespace and OVN-Kubernetes UserDefinedNetwork for isolation.

🤖 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/controller/tenant_controller.go` around lines 154 - 170, The
handleDelete method in TenantReconciler removes the tenantFinalizer prematurely
before cleaning up the associated target namespace and UserDefinedNetwork
resources that the controller owns, which can orphan isolation resources. Modify
the handleDelete method to add cleanup logic that deletes or confirms removal of
the target Namespace and UserDefinedNetwork resources before calling
controllerutil.RemoveFinalizer on the tenantFinalizer. Only return success after
both the namespace and UDN cleanup operations have completed successfully.

Source: Coding guidelines

🤖 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 `@config/rbac/role.yaml`:
- Around line 20-27: The current ClusterRole is granting cluster-wide read and
watch access to secrets, violating the principle of least privilege. Remove the
apiGroups/resources/verbs block that grants get, list, and watch access to
secrets from this cluster-wide manager role. Create a separate namespaced Role
(not ClusterRole) in the storage config namespace that contains only the secrets
access rules with the same get, list, and watch verbs. Then create a
corresponding RoleBinding in that storage namespace that binds this Role to the
appropriate service account, ensuring the storage controller only has scoped
access to secrets within its own namespace rather than across the entire
cluster.

In `@internal/controller/storage_controller.go`:
- Around line 634-642: The needsProvisionJob function returns true when
latestJob.State.IsSuccessful() is true, which causes redundant provision jobs to
be triggered if the expected resource hasn't appeared yet. Modify the
needsProvisionJob function to implement a backoff mechanism by checking a
timestamp-based condition on successful jobs, or add additional state tracking
(such as a LastProvisionTime or AwaitingResource indicator) to prevent immediate
re-triggering of provision jobs after a successful job completion. This will
avoid multiple redundant AAP jobs when there's a delay in external resource
creation.
- Around line 704-706: The mapClusterOrderToTenant method in the
StorageReconciler always returns nil, which prevents ClusterOrder changes from
triggering Tenant reconciliation. Either add a TODO comment above the method
explaining why the mapping is not yet implemented (if this is placeholder code
for future use), or remove the ClusterOrder watch registration entirely if it is
no longer needed. Determine the intent of this watcher and take the appropriate
action to either document the incomplete implementation or clean up the unused
watch.

In `@internal/controller/tenant_controller_test.go`:
- Around line 67-72: The AfterEach function currently only deletes the Tenant
resource but does not clean up the Namespace that was created in BeforeEach. Add
explicit cleanup for the Namespace in the AfterEach block by retrieving the
namespace object (similar to how the tenant is retrieved) and deleting it if it
exists, ensuring tests are fully self-contained and prevent ordering issues from
test interdependencies.
- Around line 104-108: The test at lines 104-108 currently only asserts that
StorageClasses and Jobs are nil after getting the tenant, but per the PR
changes, TenantStatus now includes StorageBackends and ClusterStorage arrays.
Add two additional Expect assertions (following the same pattern as the existing
StorageClasses and Jobs assertions) to verify that tenant.Status.StorageBackends
and tenant.Status.ClusterStorage are also nil or empty, ensuring complete
coverage of all storage-related status fields remaining untouched.

In `@internal/controller/tenant_controller.go`:
- Around line 46-48: The TenantReconciler's handleUpdate method currently only
retrieves the target namespace and immediately marks the Tenant as Ready without
actually reconciling the namespace and UDN (User Defined Network) resources
required for tenant isolation. This causes new tenants to never have their
namespace/UDN created, and allows status updates from the StorageReconciler to
override the aggregate readiness state. Modify the reconciliation logic within
handleUpdate (around lines 127-150) to actively reconcile both the target
Namespace and the associated OVN-Kubernetes UserDefinedNetwork, and ensure the
Tenant's Ready status is derived from all required readiness conditions
(including StorageBackendReady and ClusterStorageReady) rather than just
checking namespace existence. This prevents downstream ComputeInstances from
proceeding without storage being fully ready.

---

Outside diff comments:
In `@internal/controller/tenant_controller.go`:
- Around line 154-170: The handleDelete method in TenantReconciler removes the
tenantFinalizer prematurely before cleaning up the associated target namespace
and UserDefinedNetwork resources that the controller owns, which can orphan
isolation resources. Modify the handleDelete method to add cleanup logic that
deletes or confirms removal of the target Namespace and UserDefinedNetwork
resources before calling controllerutil.RemoveFinalizer on the tenantFinalizer.
Only return success after both the namespace and UDN cleanup operations have
completed successfully.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: ed74f7b0-d449-464e-afbd-4009c83acde4

📥 Commits

Reviewing files that changed from the base of the PR and between 4771f5f and e940a10.

📒 Files selected for processing (23)
  • api/v1alpha1/job_types.go
  • api/v1alpha1/tenant_types.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • cmd/main.go
  • config/crd/bases/osac.openshift.io_clusterorders.yaml
  • config/crd/bases/osac.openshift.io_computeinstances.yaml
  • config/crd/bases/osac.openshift.io_publicipattachments.yaml
  • config/crd/bases/osac.openshift.io_publicippools.yaml
  • config/crd/bases/osac.openshift.io_publicips.yaml
  • config/crd/bases/osac.openshift.io_securitygroups.yaml
  • config/crd/bases/osac.openshift.io_subnets.yaml
  • config/crd/bases/osac.openshift.io_tenants.yaml
  • config/crd/bases/osac.openshift.io_virtualnetworks.yaml
  • config/rbac/role.yaml
  • internal/controller/computeinstance_controller.go
  • internal/controller/computeinstance_controller_test.go
  • internal/controller/storage_controller.go
  • internal/controller/storage_tier_resolution.go
  • internal/controller/tenant_controller.go
  • internal/controller/tenant_controller_test.go
  • internal/controller/tenant_names.go
  • pkg/aap/client.go
  • pkg/provisioning/aap_provider.go

Comment thread config/rbac/role.yaml Outdated
Comment thread internal/controller/storage_controller.go Outdated
Comment thread internal/controller/storage_controller.go
Comment thread internal/controller/tenant_controller_test.go Outdated
Comment thread internal/controller/tenant_controller_test.go
Comment thread internal/controller/tenant_controller.go
@zszabo-rh

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@akshaynadkarni akshaynadkarni self-assigned this Jun 17, 2026
@akshaynadkarni
akshaynadkarni self-requested a review June 17, 2026 20:08

// --- Stage 2: Class provisioning ---

func (r *StorageReconciler) handleClassProvisioning(ctx context.Context, instance *v1alpha1.Tenant) (ctrl.Result, error) {

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: handleClassProvisioning / handleClassDeprovisioning / pollClassProvisionJob use "Class" terminology, but everything else in this PR uses "ClusterStorage" (condition ClusterStorageReady, job type JobTypeClusterStorageProvision, provider field ClusterStorageProvider, env vars OSAC_STORAGE_CLUSTER_AAP_*).

Looks like a leftover from before the design alignment commit renamed StorageClassReady to ClusterStorageReady. Consider renaming to handleClusterStorageProvisioning / handleClusterStorageDeprovisioning / pollClusterStorageProvisionJob for consistency.

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.

You are absolutely right about this, fixing!

Comment thread config/rbac/role.yaml Outdated
verbs:
- get
- list
- watch

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.

Can we scope this with a namespaced Role + RoleBinding in osac-system instead of adding Secrets to the cluster-wide ClusterRole? That way the operator can only read Secrets in the namespace it actually needs, without changing the pod or service account.

Something like a Role in osac-system granting secrets get/list/watch, plus a RoleBinding pointing to the existing controller-manager ServiceAccount. The rest of the ClusterRole stays as-is.

WDYT?

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 call, adding a namespaced Role + RoleBinding in osac-system for Secrets access and removing it from the ClusterRole.

@akshaynadkarni

akshaynadkarni commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@zszabo-rh The storage lifecycle logic moved from the tenant controller to StorageReconciler, but the corresponding unit tests were deleted rather than moved. Is there a plan to bring those back?

Comment thread internal/controller/storage_controller.go
Comment thread internal/controller/storage_controller.go
@akshaynadkarni

akshaynadkarni commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@zszabo-rh I didn't realize that we would need new job types for storage. This adds 4 storage-specific values (storage-backend-provision, storage-backend-deprovision, cluster-storage-provision, cluster-storage-deprovision) to the shared JobType enum, which leaks into all 9 CRD schemas. Every other controller in the operator uses the standard provision/deprovision pair. The enum is defined once on JobType in job_types.go, so there's no way to scope the new values to the Tenant CRD only.

The root cause is that two independent lifecycles (backend and cluster storage) share the Tenant CR, so they need different job types to distinguish their operations. A couple of alternatives worth discussing:

  1. Per-tenant CRs: A TenantStorageBackend CR and a TenantClusterStorage CR, each with its own controller using standard provision/deprovision. This follows the ClusterOrder/Subnet pattern. For CaaS with multiple clusters per tenant, we'd need to decide whether cluster storage is a per-cluster resource or managed differently.

  2. Keep the current approach: Accept the new job types as a pragmatic choice. The playbooks are already split, the controller works, and the enum values are validated by the CRD schema.

Given that v0.1 needs to support CaaS, what's the right decomposition here? WDYT?

cc: @avishayt @rgolangh

return false
}
return latestJob.State.IsSuccessful()
}

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 local needsProvisionJob re-triggers an AAP job when the previous one succeeded. But the failure case is already handled before this function is called (lines 287-291), so the default provisioning.NeedsProvisionJob from job_helpers.go would work here:

  • No job: triggers first job (same behavior)
  • Running: falls through to poll (same behavior)
  • Failed: never reaches this function, handled at line 287 (same behavior)
  • Succeeded: default returns false, falls through to pollBackendProvisionJob, which requeues after 5s. On the next reconcile, hubSecretExists() checks if the Secret appeared. No redundant AAP jobs.

Is there a reason the local function was needed instead of the shared one? WDYT?

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.

Yes, seems I've badly overlooked this one.. Switching to provisioning.NeedsProvisionJob!

Comment thread internal/controller/tenant_names.go Outdated

func storageClassTenantPredicate() predicate.Predicate {
return predicate.NewPredicateFuncs(func(obj client.Object) bool {
_, exists := obj.GetLabels()[osacTenantAnnotation]

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: osacTenantAnnotation is used as a label key everywhere (client.MatchingLabels, obj.GetLabels()), not as an annotation. Should this be renamed to osacTenantLabel?

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.

This one is coming from upstream, but I agree, renaming to osacTenantLabel.

@zszabo-rh

Copy link
Copy Markdown
Contributor Author

The storage lifecycle logic moved from the tenant controller to StorageReconciler, but the corresponding unit tests were deleted rather than moved. Is there a plan to bring those back?

You're right, the tests should have been moved with the code. Adding unit tests for the StorageReconciler in this PR, following the test plan from the design spec. Will push shortly.

@zszabo-rh

Copy link
Copy Markdown
Contributor Author

@akshaynadkarni about the new job types:
I share the concern, actually this was the part of the implementation I hated the most. The design spec didn't address how to distinguish job types in the shared array, and custom enum values were the quickest implementation path, but the CRD pollution is a real mess.

I've analyzed alternatives and want to discuss two options with you. Sending details via DM, short version:

  1. per-lifecycle CRDs that you also suggested (also my preferred solution), or
  2. separate Jobs arrays on Tenant as a bridge.

Both would eliminate the enum pollution.

Comment thread cmd/main.go
aapInsecureSkipVerify := helpers.GetEnvWithDefault(envAAPInsecureSkipVerify, false)

backendProvisionTemplate := helpers.GetEnvWithDefault(
envStorageBackendProvisionTemplate, "osac-create-tenant-storage-backend")

@rgolangh rgolangh Jun 18, 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.

what does the default template do? can there be a default which will bring us to a functional state for storage?

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.

No, the default doesn't bring you to a functional state on its own, it's just a convention that avoids extra env vars when using the standard AAP deployment (same pattern as all other controllers).

The full prerequisite chain is: controller enabled → AAP configured → templates deployed via config-as-code → STORAGE_TIERS env var set on instance group → VAST appliance reachable with valid credentials.

@zszabo-rh
zszabo-rh force-pushed the feat/OSAC-23-storage-controller branch from 5b19dca to f9100bc Compare June 18, 2026 08:03

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

🤖 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 `@api/v1alpha1/job_types.go`:
- Around line 24-37: The JobType enum is shared across multiple CRDs but now
contains storage-specific values (JobTypeStorageBackendProvision,
JobTypeStorageBackendDeprovision, JobTypeClusterStorageProvision,
JobTypeClusterStorageDeprovision) that should not be accepted by all resources
using JobStatus. To preserve per-resource validation boundaries, create a
separate storage-specific job type contract instead of mixing storage operations
with the generic JobType. This means defining a new type (e.g., StorageJobType)
with its own enum values and kubebuilder validation constraint, then use this
specialized type only in storage-related CRDs while keeping JobType for generic
provisioning/deprovisioning operations across all resources.

In `@config/crd/bases/osac.openshift.io_securitygroups.yaml`:
- Around line 275-278: The storage-backend-provision,
storage-backend-deprovision, cluster-storage-provision, and
cluster-storage-deprovision job type values should not be part of the
SecurityGroup CRD schema since they are tenant-storage lifecycle operations, not
security group operations. Remove these four enum values from the jobs[].type
field definition in the SecurityGroup CRD to properly scope job types to only
security group-related operations and strengthen schema-level contract
validation.

In `@config/rbac/storage_secrets_role_binding.yaml`:
- Around line 13-16: Remove the explicit namespace field from the ServiceAccount
subject in the RoleBinding resource where the ServiceAccount subject has kind
ServiceAccount and name controller-manager. Additionally, remove the explicit
namespace field from the ServiceAccount resource definition itself in
config/rbac/service_account.yaml. This allows Kustomize to apply the namespace
transformation uniformly across all RBAC resources based on the configuration in
config/default/kustomization.yaml.

In `@internal/controller/storage_controller_test.go`:
- Around line 83-95: The createLabeledStorageClass helper function creates
StorageClass objects but never cleans them up, causing accumulation across tests
and potential flakiness since StorageClass is cluster-scoped. Add cleanup logic
to the createLabeledStorageClass function to delete the StorageClass after
creation, either by using a defer statement with k8sClient.Delete to ensure the
StorageClass is removed after the test completes, or by returning a cleanup
function that the test caller can use. This ensures that StorageClass objects do
not persist and interfere with subsequent tests.
- Around line 396-400: The test block with the Eventually call around the
Reconcile method invocation only verifies that Reconcile executes without error,
but does not actually assert that the Tenant resource is deleted or that the
storageFinalizer is removed. Add an additional assertion within the Eventually
block after the Reconcile call to verify that the Tenant is either deleted
(using Get with NotFound expectation) or that its metadata no longer contains
the storageFinalizer. This ensures the test comprehensively validates the
deletion and finalizer cleanup behavior.

In `@internal/controller/storage_controller.go`:
- Around line 506-565: The delete flow currently allows the finalizer removal to
proceed after a successful backend deprovision job without verifying that the
hub Secret has actually been deleted, which can leave credentials orphaned.
Modify the logic so that even after the backend deprovision job completes
successfully (when polling in pollDeprovisionJob returns or when handling the
DeprovisionTriggered action), the delete flow does not proceed until
hubSecretReady confirms the hub Secret is actually gone. Ensure the condition at
line 520 (the check for !hubSecretReady) remains a blocking requirement
throughout the delete process, preventing finalizer removal until the Secret
deletion is confirmed.
- Around line 207-229: When cluster storage resolution fails (when
len(result.resolved) == 0), the code sets TenantConditionClusterStorageReady to
False but leaves stale data in instance.Status.StorageClasses. In the unresolved
path (the if len(result.resolved) == 0 block), after setting the status
condition to false, explicitly clear instance.Status.StorageClasses by setting
it to an empty slice or nil before the early return statement, so that the
status accurately reflects the unresolved state without stale storage class
data.

In `@internal/controller/tenant_controller.go`:
- Around line 46-48: The TenantReconciler docstring (lines 46-48) is outdated
and no longer reflects the current implementation. The comment claims the
reconciler manages namespace creation and UDN reconciliation, but after the
refactor it only tracks namespace readiness and lifecycle state. Update the
docstring for TenantReconciler to remove references to namespace creation and
UDN reconciliation management, and instead accurately describe that it tracks
namespace readiness and lifecycle state, while keeping the note about Storage
provisioning being handled by the OSAC Storage Controller.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 0156e81c-f79c-48d1-8925-e08684ff42d7

📥 Commits

Reviewing files that changed from the base of the PR and between e940a10 and f9100bc.

📒 Files selected for processing (37)
  • api/v1alpha1/job_types.go
  • api/v1alpha1/tenant_types.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • charts/operator-crds/templates/osac.openshift.io_clusterorders.yaml
  • charts/operator-crds/templates/osac.openshift.io_computeinstances.yaml
  • charts/operator-crds/templates/osac.openshift.io_publicipattachments.yaml
  • charts/operator-crds/templates/osac.openshift.io_publicippools.yaml
  • charts/operator-crds/templates/osac.openshift.io_publicips.yaml
  • charts/operator-crds/templates/osac.openshift.io_securitygroups.yaml
  • charts/operator-crds/templates/osac.openshift.io_subnets.yaml
  • charts/operator-crds/templates/osac.openshift.io_tenants.yaml
  • charts/operator-crds/templates/osac.openshift.io_virtualnetworks.yaml
  • cmd/main.go
  • config/crd/bases/osac.openshift.io_clusterorders.yaml
  • config/crd/bases/osac.openshift.io_computeinstances.yaml
  • config/crd/bases/osac.openshift.io_publicipattachments.yaml
  • config/crd/bases/osac.openshift.io_publicippools.yaml
  • config/crd/bases/osac.openshift.io_publicips.yaml
  • config/crd/bases/osac.openshift.io_securitygroups.yaml
  • config/crd/bases/osac.openshift.io_subnets.yaml
  • config/crd/bases/osac.openshift.io_tenants.yaml
  • config/crd/bases/osac.openshift.io_virtualnetworks.yaml
  • config/rbac/kustomization.yaml
  • config/rbac/storage_secrets_role.yaml
  • config/rbac/storage_secrets_role_binding.yaml
  • internal/controller/computeinstance_controller.go
  • internal/controller/computeinstance_controller_test.go
  • internal/controller/computeinstance_integration_test.go
  • internal/controller/computeinstance_resources.go
  • internal/controller/storage_controller.go
  • internal/controller/storage_controller_test.go
  • internal/controller/storage_tier_resolution.go
  • internal/controller/tenant_controller.go
  • internal/controller/tenant_controller_test.go
  • internal/controller/tenant_names.go
  • pkg/aap/client.go
  • pkg/provisioning/aap_provider.go

Comment thread api/v1alpha1/job_types.go Outdated
Comment thread config/crd/bases/osac.openshift.io_securitygroups.yaml Outdated
Comment thread config/rbac/storage_secrets_role_binding.yaml
Comment thread internal/controller/storage_controller_test.go
Comment thread internal/controller/storage_controller_test.go
Comment thread internal/controller/storage_controller.go
Comment thread internal/controller/storage_controller.go Outdated
Comment thread internal/controller/tenant_controller.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@internal/controller/storage_controller_test.go`:
- Around line 95-97: The DeferCleanup function on lines 95-97 is silently
dropping the error from k8sClient.Delete by assigning it to underscore, which
can leak cluster-scoped StorageClass test state. Instead of ignoring the error,
check it using client.IgnoreNotFound to ensure that only NotFound errors are
acceptable and other deletion errors are properly handled or asserted. Apply the
same fix to the similar code on lines 403-406 where Get errors are incorrectly
being treated as passing deletion/finalizer checks. Replace underscore error
assignments with proper error checks that use client.IgnoreNotFound to
distinguish between expected NotFound errors and unexpected errors that should
fail the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: e0b1fbf7-9aa5-4754-822e-a67145536313

📥 Commits

Reviewing files that changed from the base of the PR and between f9100bc and 0526f9e.

📒 Files selected for processing (3)
  • internal/controller/storage_controller.go
  • internal/controller/storage_controller_test.go
  • internal/controller/tenant_controller.go

Comment thread internal/controller/storage_controller_test.go
@zszabo-rh
zszabo-rh force-pushed the feat/OSAC-23-storage-controller branch from 0526f9e to a448ff6 Compare June 18, 2026 09:41
@rgolangh

Copy link
Copy Markdown
Contributor

@zszabo-rh I didn't realize that we would need new job types for storage. This adds 4 storage-specific values (storage-backend-provision, storage-backend-deprovision, cluster-storage-provision, cluster-storage-deprovision) to the shared JobType enum, which leaks into all 9 CRD schemas. Every other controller in the operator uses the standard provision/deprovision pair. The enum is defined once on JobType in job_types.go, so there's no way to scope the new values to the Tenant CRD only.

The root cause is that two independent lifecycles (backend and cluster storage) share the Tenant CR, so they need different job types to distinguish their operations. A couple of alternatives worth discussing:

1. **Per-tenant CRs**: A `TenantStorageBackend` CR and a `TenantClusterStorage` CR, each with its own controller using standard `provision`/`deprovision`. This follows the ClusterOrder/Subnet pattern. For CaaS with multiple clusters per tenant, we'd need to decide whether cluster storage is a per-cluster resource or managed differently.

2. **Keep the current approach**: Accept the new job types as a pragmatic choice. The playbooks are already split, the controller works, and the enum values are validated by the CRD schema.

Given that v0.1 needs to support CaaS, what's the right decomposition here? WDYT?

cc: @avishayt @rgolangh

I wouldn't wait for that for 0.1 but sub-nets having storage job types is a smell and we should change it

@akshaynadkarni

akshaynadkarni commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

@zszabo-rh I didn't realize that we would need new job types for storage. This adds 4 storage-specific values (storage-backend-provision, storage-backend-deprovision, cluster-storage-provision, cluster-storage-deprovision) to the shared JobType enum, which leaks into all 9 CRD schemas. Every other controller in the operator uses the standard provision/deprovision pair. The enum is defined once on JobType in job_types.go, so there's no way to scope the new values to the Tenant CRD only.

The root cause is that two independent lifecycles (backend and cluster storage) share the Tenant CR, so they need different job types to distinguish their operations. A couple of alternatives worth discussing:

  1. Per-tenant CRs: A TenantStorageBackend CR and a TenantClusterStorage CR, each with its own controller using standard provision/deprovision. This follows the ClusterOrder/Subnet pattern. For CaaS with multiple clusters per tenant, we'd need to decide whether cluster storage is a per-cluster resource or managed differently.
  2. Keep the current approach: Accept the new job types as a pragmatic choice. The playbooks are already split, the controller works, and the enum values are validated by the CRD schema.

Given that v0.1 needs to support CaaS, what's the right decomposition here? WDYT?

cc: @avishayt @rgolangh

Circulated the document CRD_JobTrackingAlternatives and got agreement on Option C.
cc: @zszabo-rh

@zszabo-rh
zszabo-rh force-pushed the feat/OSAC-23-storage-controller branch from a448ff6 to 6535639 Compare June 19, 2026 04:22
zszabo-rh and others added 9 commits June 22, 2026 13:22
- Fix gofmt formatting in main.go, storage_tier_resolution.go, tenant_controller_test.go
- Break long lines in setupStorageController to stay under 120 chars
- Remove unused eventReasonStorageClassNotReady const
- Fix namespace collision in tenant test (IgnoreAlreadyExists)
- Sync Helm CRD templates from config/crd/bases
- Remove extra blank line in tenant_types.go

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Zoltan Szabo <zszabo@redhat.com>
- Use `make helm-crds` to properly sync CRDs with Helm wrappers
- Handle AlreadyExists for Tenant CR in BeforeEach (same envtest
  race as namespace)

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Zoltan Szabo <zszabo@redhat.com>
Each It test case now uses a unique tenant/namespace name to avoid
envtest cleanup races. The shared BeforeEach/AfterEach pattern caused
finalizer-related interference between test cases.

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Zoltan Szabo <zszabo@redhat.com>
- Rename handleClassProvisioning→handleClusterStorageProvisioning and
  related functions for naming consistency with ClusterStorageReady
- Replace local needsProvisionJob with shared provisioning.NeedsProvisionJob
  to avoid redundant AAP job triggers on success
- Add TODO(OSAC-1123) for ClusterOrder-to-Tenant watch placeholder
- Rename osacTenantAnnotation→osacTenantKey with updated comment
  reflecting dual usage as label and annotation
- Scope Secrets RBAC to osac-system namespace via Role+RoleBinding
  instead of cluster-wide ClusterRole entry

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Zoltan Szabo <zszabo@redhat.com>
Covers the design spec test plan:
- Stage 1: tenant not ready (skip), no hub Secret (trigger provision),
  Secret exists (skip to Stage 2), no provider (NoProvider condition),
  job failure (failed job recorded)
- Stage 2: Stage 1 complete + no SCs (trigger provision), SCs
  discovered (ClusterStorageReady=True)
- Tier resolution: Default fallback, tenant-specific priority
- Finalizer: added on first reconcile, deletion runs without class provider
- Management state: Unmanaged skips reconciliation

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Zoltan Szabo <zszabo@redhat.com>
- Clear status.storageClasses when no SCs resolved (prevents stale data
  when ClusterStorageReady=False)
- Fix tenant controller docstring (tracks readiness, doesn't create)
- Add DeferCleanup for StorageClasses in tests (prevent cross-test leak)
- Strengthen deletion test to assert finalizer removal

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Zoltan Szabo <zszabo@redhat.com>
…all CRDs

Rename status.jobs to status.provisioningJobs on all 9 CRDs to establish
a uniform naming pattern. Add lifecycle-specific job arrays on Tenant
(storageBackendJobs, clusterStorageJobs) and ClusterOrder
(clusterStorageJobs) so storage controllers use standard provision/
deprovision job types scoped by array instead of polluting the shared
JobType enum.

Changes:
- Remove 4 storage-specific JobType enum values
- Rename Jobs -> ProvisioningJobs in all CRD status structs
- Split storage controller job tracking into per-lifecycle arrays
- Refactor pollDeprovisionJob to accept jobs parameter
- Add 4 new unit tests for job array isolation and failure paths
- Regenerate CRDs and Helm chart

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Zoltan Szabo <zszabo@redhat.com>
Make the provisioning lifecycle helpers lifecycle-agnostic so the
storage controller and ComputeInstance can use them directly instead
of reimplementing ~400 lines of manual job management.

Changes:
- Add JobsExtractor type for CheckAPIServerForNonTerminalProvisionJob
  to support lifecycle-specific job array extraction from fresh resources
- Add provisionJobs parameter to TriggerDeprovision interface so the
  provider receives the correct provision jobs for pre-deprovision checks
  instead of re-extracting via GetJobsFromResource
- Replace storage controller's 6 manual provisioning/deprovisioning
  methods with RunProvisioningLifecycle and RunDeprovisioningLifecycle
- Replace ComputeInstance's handleDeprovisioning (~100 lines) with
  RunDeprovisioningLifecycle call
- Drop fake failed jobs on trigger errors (align with standard behavior)
- Keep wait-for-external-trigger on failure (storage-specific pre-check)

Net: -346 lines (482 removed, 136 added)

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Zoltan Szabo <zszabo@redhat.com>
Format long JobsExtractor inline functions with gofmt and remove
the unused deprovisioningJobTriggeredMessage constant.

Assisted-by: Cursor/Claude
Signed-off-by: akshaynadkarni <25892229+akshaynadkarni@users.noreply.github.com>
@akshaynadkarni
akshaynadkarni force-pushed the feat/OSAC-23-storage-controller branch from b385bed to 8e66343 Compare June 22, 2026 17:23
Format long JobsExtractor inline functions in publicippool,
securitygroup, and virtualnetwork controllers.

Assisted-by: Cursor/Claude
Signed-off-by: akshaynadkarni <25892229+akshaynadkarni@users.noreply.github.com>
Assisted-by: Cursor/Claude
Signed-off-by: akshaynadkarni <25892229+akshaynadkarni@users.noreply.github.com>
@akshaynadkarni

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

🤖 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 `@cmd/main.go`:
- Around line 434-443: The StorageReconciler is being initialized with
potentially nil backendProvider and clusterStorageProvider values when aapURL or
aapToken are empty, but there is no logging to indicate this watch-only mode.
Add an informational log message before the NewStorageReconciler call to check
if either backendProvider or clusterStorageProvider are nil, and if so, log a
message indicating that the storage controller is running in watch-only mode
without AAP integration capability. This ensures operators are aware when the
controller lacks provisioning capabilities.

In `@internal/controller/computeinstance_controller.go`:
- Around line 467-474: The code calls RunDeprovisioningLifecycle with
r.ProvisioningProvider without first checking if the provider is nil, which can
cause the delete reconcile to fail. Add a nil check for r.ProvisioningProvider
before the RunDeprovisioningLifecycle call, similar to the existing pattern used
for checking the management-state annotation, and return early if the provider
is not set to safely skip the deprovision operation when the provider is unset.

In `@internal/controller/storage_tier_resolution.go`:
- Around line 49-54: The joinStorageClassNames function returns storage class
names in API list order, which can vary across reconciles and cause the
duplicate-tier messages at lines 123 and 143 to flip ordering and create
unnecessary status churn. Sort the names slice alphabetically before returning
it from joinStorageClassNames to ensure consistent ordering of storage class
names regardless of API list order. This prevents status updates caused by
message composition changes across reconciles.

In `@pkg/provisioning/provision_lifecycle_test.go`:
- Around line 49-50: The TriggerDeprovision method in mockProvider accepts the
provisionJobs parameter but does not pass it to triggerDeprovisionFunc, making
it impossible to test deprovision job-history plumbing. Update the
triggerDeprovisionFunc function type signature to accept the provisionJobs
parameter and then pass it when calling triggerDeprovisionFunc in the
TriggerDeprovision method implementation so that test cases can verify the job
history is properly handled during deprovisioning.

In `@pkg/provisioning/provision_lifecycle.go`:
- Around line 79-84: The function `CheckAPIServerForNonTerminalProvisionJob`
calls the `extract` function parameter unconditionally on line 84, but if
`extract` is nil, this will panic the controller. Add a nil check for the
`extract` parameter before calling `extract(fresh)` and return false if the
extractor is not provided to safely skip the check instead of panicking.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 7cbcbc1d-b2d3-4a47-a5fa-ac37f9677cac

📥 Commits

Reviewing files that changed from the base of the PR and between 6535639 and e516b1a.

📒 Files selected for processing (73)
  • .gitignore
  • api/v1alpha1/clusterorder_types.go
  • api/v1alpha1/computeinstance_types.go
  • api/v1alpha1/publicip_types.go
  • api/v1alpha1/publicipattachment_types.go
  • api/v1alpha1/publicipattachment_types_test.go
  • api/v1alpha1/publicippool_types.go
  • api/v1alpha1/securitygroup_types.go
  • api/v1alpha1/subnet_types.go
  • api/v1alpha1/tenant_types.go
  • api/v1alpha1/virtualnetwork_types.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • charts/operator-crds/templates/osac.openshift.io_clusterorders.yaml
  • charts/operator-crds/templates/osac.openshift.io_computeinstances.yaml
  • charts/operator-crds/templates/osac.openshift.io_publicipattachments.yaml
  • charts/operator-crds/templates/osac.openshift.io_publicippools.yaml
  • charts/operator-crds/templates/osac.openshift.io_publicips.yaml
  • charts/operator-crds/templates/osac.openshift.io_securitygroups.yaml
  • charts/operator-crds/templates/osac.openshift.io_subnets.yaml
  • charts/operator-crds/templates/osac.openshift.io_tenants.yaml
  • charts/operator-crds/templates/osac.openshift.io_virtualnetworks.yaml
  • charts/operator/templates/deployment.yaml
  • charts/operator/templates/storage-secrets-role.yaml
  • charts/operator/templates/storage-secrets-rolebinding.yaml
  • cmd/main.go
  • config/crd/bases/osac.openshift.io_clusterorders.yaml
  • config/crd/bases/osac.openshift.io_computeinstances.yaml
  • config/crd/bases/osac.openshift.io_publicipattachments.yaml
  • config/crd/bases/osac.openshift.io_publicippools.yaml
  • config/crd/bases/osac.openshift.io_publicips.yaml
  • config/crd/bases/osac.openshift.io_securitygroups.yaml
  • config/crd/bases/osac.openshift.io_subnets.yaml
  • config/crd/bases/osac.openshift.io_tenants.yaml
  • config/crd/bases/osac.openshift.io_virtualnetworks.yaml
  • config/rbac/kustomization.yaml
  • config/rbac/storage_secrets_role.yaml
  • config/rbac/storage_secrets_role_binding.yaml
  • internal/controller/clusterorder_controller.go
  • internal/controller/clusterorder_controller_test.go
  • internal/controller/clusterorder_integration_test.go
  • internal/controller/computeinstance_controller.go
  • internal/controller/computeinstance_controller_test.go
  • internal/controller/computeinstance_integration_test.go
  • internal/controller/computeinstance_provisioning_test.go
  • internal/controller/computeinstance_resources.go
  • internal/controller/constants_common.go
  • internal/controller/publicip_controller.go
  • internal/controller/publicip_controller_test.go
  • internal/controller/publicipattachment_controller.go
  • internal/controller/publicipattachment_controller_test.go
  • internal/controller/publicippool_controller.go
  • internal/controller/publicippool_controller_test.go
  • internal/controller/securitygroup_controller.go
  • internal/controller/securitygroup_controller_test.go
  • internal/controller/storage_controller.go
  • internal/controller/storage_controller_test.go
  • internal/controller/storage_tier_resolution.go
  • internal/controller/subnet_controller.go
  • internal/controller/subnet_controller_test.go
  • internal/controller/suite_test.go
  • internal/controller/tenant_controller.go
  • internal/controller/tenant_controller_test.go
  • internal/controller/tenant_names.go
  • internal/controller/virtualnetwork_controller.go
  • internal/controller/virtualnetwork_controller_test.go
  • pkg/aap/client.go
  • pkg/provisioning/aap_provider.go
  • pkg/provisioning/aap_provider_test.go
  • pkg/provisioning/factory.go
  • pkg/provisioning/factory_test.go
  • pkg/provisioning/provider.go
  • pkg/provisioning/provision_lifecycle.go
  • pkg/provisioning/provision_lifecycle_test.go
💤 Files with no reviewable changes (1)
  • internal/controller/constants_common.go

Comment thread cmd/main.go
Comment thread internal/controller/computeinstance_controller.go
Comment thread internal/controller/storage_tier_resolution.go
Comment thread pkg/provisioning/provision_lifecycle_test.go
Comment thread pkg/provisioning/provision_lifecycle.go
@akshaynadkarni

Copy link
Copy Markdown
Contributor

@coderabbitai resolve

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

@akshaynadkarni

akshaynadkarni commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@omer-vishlitzky @eliorerz

The e2e-vmaas Prow job on this PR is failing because the PR renames status.jobs to status.provisioningJobs across all CRDs. The operator image is built from the PR branch, but the CRDs are deployed from the osac-installer submodule, which is pinned to main. The operator writes to status.provisioningJobs, the CRD schema still has status.jobs, and the status updates are silently dropped.

From the operator logs:

INFO  unknown field "status.provisioningJobs"  {"controller": "publicippool"}
INFO  unknown field "status.provisioningJobs"  {"controller": "virtualnetwork"}

This repeats every 30 seconds. Because the operator cannot persist job tracking state, it launches a new AAP provision job on every reconcile cycle, creating an infinite loop.

Manual E2E validation on edge22:

I ran the full e2e-vmaas suite on my beaker machine with the correct CRDs applied from the PR branch. 46 passed, 1 failed. There was 1 failure (test_validation_rejections) due to edge22 being a single-node cluster that cannot schedule two concurrent 4GiB VMs. However, the test passes when re-run individually.

All provisioning controllers work correctly with the renamed fields: VirtualNetwork, Subnet, SecurityGroup, ComputeInstance, PublicIPPool, and PublicIPAttachment all pass.

Code tested:
  osac-operator:       e516b1a (branch: feat/OSAC-23-storage-controller)
  fulfillment-service: 82eca3f (branch: main)
  osac-aap:            f130cf4 (branch: main)

I'd like to /override the Prow e2e failure for this PR.


Question
For future PRs that change CRD schemas, we need a way to apply the CRDs from the PR branch before the operator starts. Is there a hook in the Prow step registry or the cluster-tool-boot step where we can run oc apply -f config/crd/bases/ from the PR checkout? This would prevent any CRD-rename PR from hitting the same issue.

cc: @zszabo-rh

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

Changes LGTM

@openshift-ci openshift-ci Bot added the lgtm label Jun 23, 2026
@openshift-ci

openshift-ci Bot commented Jun 23, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: akshaynadkarni, zszabo-rh

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

@omer-vishlitzky

Copy link
Copy Markdown
Contributor

/override ci/prow/e2e-vmaas

@openshift-ci

openshift-ci Bot commented Jun 23, 2026

Copy link
Copy Markdown

@omer-vishlitzky: Overrode contexts on behalf of omer-vishlitzky: ci/prow/e2e-vmaas

Details

In response to this:

/override ci/prow/e2e-vmaas

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 kubernetes-sigs/prow repository.

@openshift-merge-bot
openshift-merge-bot Bot merged commit 658dfc3 into osac-project:main Jun 23, 2026
11 checks passed
@openshift-ci

openshift-ci Bot commented Jun 23, 2026

Copy link
Copy Markdown

@zszabo-rh: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/e2e-vmaas e516b1a link unknown /test e2e-vmaas

Full PR test history. Your PR dashboard.

Details

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 kubernetes-sigs/prow repository. I understand the commands that are listed here.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants