Skip to content

🌱 Externalize CER phase objects into Secrets#2595

Open
pedjak wants to merge 1 commit intooperator-framework:mainfrom
pedjak:large-bundle-support
Open

🌱 Externalize CER phase objects into Secrets#2595
pedjak wants to merge 1 commit intooperator-framework:mainfrom
pedjak:large-bundle-support

Conversation

@pedjak
Copy link
Contributor

@pedjak pedjak commented Mar 25, 2026

Description

Externalize ClusterExtensionRevision phase objects into content-addressable
immutable Secrets, removing the etcd object size limit as a constraint on
bundle size.

API changes:

  • Add ObjectSourceRef type with name, namespace, and key fields
  • Make ClusterExtensionRevisionObject.Object optional (omitzero)
  • Add optional Ref field with XValidation ensuring exactly one of object or ref is set
  • Add RefResolutionFailed condition reason and RevisionNameKey label

Applier (boxcutter.go):

  • Add SecretPacker to bin-pack serialized objects into Secrets with gzip compression for objects >800KiB
  • Implement crash-safe three-step creation: create Secrets, create CER with refs, patch ownerReferences
  • Externalize desiredRevision before SSA comparison so patches compare refs-vs-refs
  • Add ensureSecretOwnerReferences for crash recovery
  • Pass SystemNamespace to Boxcutter

CER controller:

  • Add resolveObjectRef to fetch and decompress objects from Secrets
  • Handle ref resolution in buildBoxcutterPhases
  • Add RBAC for Secret get/list/watch

E2e tests:

  • Add scenario verifying refs, immutability, labels, and ownerRefs
  • Add step definitions for ref Secret validation
  • Fix listExtensionRevisionResources and ClusterExtensionRevisionObjectsNotFoundOrNotOwned to resolve refs

All 37 experimental e2e scenarios pass (330 steps).

Reviewer Checklist

  • API Go Documentation
  • Tests: Unit Tests (and E2E Tests, if appropriate)
  • Comprehensive Commit Messages
  • Links to related GitHub Issue(s)

Copilot AI review requested due to automatic review settings March 25, 2026 17:19
@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Mar 25, 2026
@openshift-ci
Copy link

openshift-ci bot commented Mar 25, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

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

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

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

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

@netlify
Copy link

netlify bot commented Mar 25, 2026

Deploy Preview for olmv1 ready!

Name Link
🔨 Latest commit 8d43ef4
🔍 Latest deploy log https://app.netlify.com/projects/olmv1/deploys/69c43cfb48106c000836ceed
😎 Deploy Preview https://deploy-preview-2595--olmv1.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@pedjak pedjak changed the title 🌱 Externalize CER phase objects into Secret refs 🌱 Externalize CER phase objects into Secrets Mar 25, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces Secret-backed references for ClusterExtensionRevision (CER) phase objects to remove etcd object-size limits from being a bundle-size constraint, updating the API/CRDs, applier, reconciler, and e2e coverage accordingly.

Changes:

  • Add ObjectSourceRef and make CER phase objects support either inline object or Secret ref (exactly one required via validation).
  • Externalize phase objects into immutable, content-addressable Secrets during revision apply, and resolve refs in the CER controller (with gzip auto-detection).
  • Extend e2e and unit tests to validate refs, Secret immutability/labels/ownerRefs, and ref resolution behavior.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
api/v1/clusterextensionrevision_types.go Adds ObjectSourceRef, makes object optional, adds ref, and CEL validation for exclusivity.
api/v1/clusterextensionrevision_types_test.go Adds validity tests for inline vs ref vs invalid combinations.
api/v1/zz_generated.deepcopy.go Updates deepcopy behavior for the new Ref field/type.
applyconfigurations/api/v1/objectsourceref.go Adds apply-configuration type for ObjectSourceRef.
applyconfigurations/api/v1/clusterextensionrevisionobject.go Adds Ref support in apply-configuration for revision objects.
applyconfigurations/utils.go Registers ObjectSourceRef apply-configuration by kind.
manifests/experimental.yaml Updates CRD schema/docs for optional object, new ref, and validation.
manifests/experimental-e2e.yaml Same CRD schema updates for the e2e manifest set.
helm/.../olm.operatorframework.io_clusterextensionrevisions.yaml Updates packaged CRD YAML for the same schema changes.
internal/operator-controller/labels/labels.go Adds RevisionNameKey label constant for listing revision-associated resources.
internal/operator-controller/applier/secretpacker.go Introduces SecretPacker to bin-pack serialized objects into immutable Secrets and produce refs.
internal/operator-controller/applier/boxcutter.go Externalizes desired revisions before SSA comparison; adds crash-safe create sequence and ownerRef patching helpers.
internal/operator-controller/applier/boxcutter_test.go Updates tests to include corev1 scheme + system namespace for Secret operations.
internal/operator-controller/applier/secretpacker_test.go Adds unit tests for packing, determinism, compression behavior, labels, immutability.
internal/operator-controller/applier/externalize_test.go Adds focused tests for apply-config conversion + inline→ref replacement behavior.
internal/operator-controller/controllers/clusterextensionrevision_controller.go Resolves Secret refs (including gzip) when building boxcutter phases; adds Secret RBAC marker.
internal/operator-controller/controllers/resolve_ref_test.go Adds unit tests exercising ref resolution paths and common failure cases.
cmd/operator-controller/main.go Passes configured SystemNamespace into Boxcutter.
test/e2e/steps/steps.go Adds steps to validate refs + referenced Secrets and updates resource listing to resolve refs.
test/e2e/features/install.feature Adds an e2e scenario asserting externalization invariants (refs, immutability, labels, ownerRefs).
docs/concepts/large-bundle-support.md Adds design/behavior documentation for large bundle support via per-object Secret refs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +100 to +104
// Gzip large objects.
if len(data) > gzipThreshold {
compressed, cErr := gzipData(data)
if cErr != nil {
return nil, fmt.Errorf("compressing object in phase %d index %d: %w", phaseIdx, objIdx, cErr)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

SecretPacker computes the ref key from data after optional gzip compression. This makes ref.key (and therefore the Secret name) depend on the encoding choice/threshold and gzip implementation details rather than the underlying manifest content. Compute the hash from the canonical serialized JSON bytes, and then (optionally) store a gzipped payload under that stable key.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +124
// If adding this entry would exceed the limit, finalize the current Secret.
if currentSize+len(data) > maxSecretDataSize && len(currentData) > 0 {
finalizeCurrent()
}

currentData[key] = data
currentSize += len(data)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

When two objects have identical content, currentData[key] = data overwrites the existing entry but currentSize is still incremented. That can cause premature Secret finalization and extra Secrets (and could even trip the size check incorrectly if many duplicates exist). Only add to currentSize when the key is new (or track size based on the map contents).

Suggested change
// If adding this entry would exceed the limit, finalize the current Secret.
if currentSize+len(data) > maxSecretDataSize && len(currentData) > 0 {
finalizeCurrent()
}
currentData[key] = data
currentSize += len(data)
// Only account for size when this is a new key in the current Secret.
if _, exists := currentData[key]; !exists {
// If adding this entry would exceed the limit, finalize the current Secret.
if currentSize+len(data) > maxSecretDataSize && len(currentData) > 0 {
finalizeCurrent()
}
currentData[key] = data
currentSize += len(data)
}

Copilot uses AI. Check for mistakes.
obj := specObj.Object.DeepCopy()
var obj *unstructured.Unstructured
switch {
case specObj.Object.Object != nil:
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

specObj.Object.Object != nil treats an empty-but-present inline object (object: {}) as set, which can lead to later failures when kind/name are missing. Since the API validation is presence-based, prefer checking for actual content (e.g., len(specObj.Object.Object) > 0) to avoid accepting empty inline objects as valid inputs here.

Suggested change
case specObj.Object.Object != nil:
case len(specObj.Object.Object) > 0:

Copilot uses AI. Check for mistakes.
Comment on lines +533 to +537
secret := &corev1.Secret{}
key := client.ObjectKey{Name: ref.Name, Namespace: ref.Namespace}
if err := c.Client.Get(ctx, key, secret); err != nil {
return nil, fmt.Errorf("getting Secret %s/%s: %w", ref.Namespace, ref.Name, err)
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

ObjectSourceRef.namespace is optional in the API/CRD, but resolveObjectRef performs Client.Get with ref.Namespace as-is. If a user supplies a ref without a namespace, this will always look in the empty namespace and fail. Either make namespace required at the API level, or implement a well-defined default (e.g., the controller/system namespace) before fetching.

Copilot uses AI. Check for mistakes.
Comment on lines +339 to +369
Step 1: Create Secret(s) with revision label, no ownerReference
|
| crash here → Orphaned Secrets exist with no owner.
| ClusterExtension controller detects them on
| next reconciliation by listing Secrets with
| the revision label and checking whether the
| corresponding CER exists. If not, deletes them.
v
Step 2: Create CER with refs pointing to the Secrets from step 1
|
| crash here → CER exists, Secrets exist, refs resolve.
| CER reconciler can proceed normally.
| Secrets have no ownerReferences yet.
| ClusterExtension controller retries step 3.
v
Step 3: Patch ownerReferences onto Secrets (using CER uid)
|
| crash here → Some Secrets have ownerRefs, some don't.
| ClusterExtension controller retries patching
| the remaining Secrets on next reconciliation.
v
Done — CER has refs, all Secrets exist with owner refs.
```
Key properties:
- **No reconciler churn**: Referenced Secrets exist before the CER is created.
The CER reconciler never encounters missing Secrets during normal operation.
- **Orphan cleanup**: Secrets created in step 1 carry the revision label
(`olm.operatorframework.io/revision-name`). If a crash leaves Secrets without
a corresponding CER, the ClusterExtension controller detects and deletes them
on its next reconciliation.
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The crash-safety section claims the controller will detect and delete orphaned Secrets created in step 1 if the corresponding CER doesn't exist. The current implementation doesn't appear to delete orphaned ref Secrets; instead, ensureSecretOwnerReferences can later adopt any Secret with the revision label. Please align the doc with the actual behavior (or implement the described orphan cleanup).

Copilot uses AI. Check for mistakes.
CER is created (see [Crash-safe creation sequence](#crash-safe-creation-sequence)).
If a referenced Secret or key is not found — indicating an inconsistent state
caused by external modification or a partially completed creation sequence —
the reconciler sets a terminal error condition on the CER.
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The doc states that if a referenced Secret/key is not found, the reconciler sets a terminal error condition on the CER. In the current controller implementation, ref resolution failures are treated as retryable (Retrying/Reconciling) and will requeue indefinitely. Please update the doc to match behavior or update the controller to make ref resolution failures terminal as described.

Suggested change
the reconciler sets a terminal error condition on the CER.
the reconciler records a transient error condition on the CER and continues to
requeue the revision for reconciliation until the reference is resolved or the
CER is deleted.

Copilot uses AI. Check for mistakes.
Comment on lines +545 to +555
if len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b {
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("creating gzip reader for key %q in Secret %s/%s: %w", ref.Key, ref.Namespace, ref.Name, err)
}
defer reader.Close()
decompressed, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("decompressing key %q in Secret %s/%s: %w", ref.Key, ref.Namespace, ref.Name, err)
}
data = decompressed
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

io.ReadAll(reader) will fully decompress whatever is stored in the Secret key. Since refs can be user-provided, this allows a gzip bomb / oversized payload to drive high memory usage in the controller. Consider enforcing a maximum decompressed size (e.g., via io.LimitReader) before unmarshalling.

Copilot uses AI. Check for mistakes.
Comment on lines +501 to +507
resolved, err := c.resolveObjectRef(ctx, specObj.Ref)
if err != nil {
return nil, nil, fmt.Errorf("resolving ref in phase %q: %w", specPhase.Name, err)
}
obj = resolved
default:
return nil, nil, fmt.Errorf("object in phase %q has neither object nor ref", specPhase.Name)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Ref resolution errors currently flow into setRetryingConditions and return an error, so a missing Secret/key will be retried indefinitely. The API adds ClusterExtensionRevisionReasonRefResolutionFailed, but it's not used here. If missing/invalid refs are meant to be terminal (as the docs suggest), set a non-retrying condition reason (e.g., RefResolutionFailed/Blocked) and avoid endless requeues.

Copilot uses AI. Check for mistakes.
Comment on lines +856 to +883
// List Secrets with the revision-name label
secretList := &corev1.SecretList{}
if err := bc.Client.List(ctx, secretList,
client.InNamespace(bc.SystemNamespace),
client.MatchingLabels{labels.RevisionNameKey: cer.Name},
); err != nil {
return fmt.Errorf("listing ref Secrets for revision %q: %w", cer.Name, err)
}

var needsPatch []corev1.Secret
for _, s := range secretList.Items {
hasOwnerRef := false
for _, ref := range s.OwnerReferences {
if ref.UID == cer.UID {
hasOwnerRef = true
break
}
}
if !hasOwnerRef {
needsPatch = append(needsPatch, s)
}
}

if len(needsPatch) == 0 {
return nil
}

return bc.patchOwnerRefsOnSecrets(ctx, cer.Name, cer.UID, needsPatch)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

ensureSecretOwnerReferences lists all Secrets in the system namespace with olm.operatorframework.io/revision-name=<cer.Name> and patches an ownerReference onto any missing it. This can accidentally adopt stale/orphan Secrets from previous failed attempts (or any Secret with that label), even if they are not referenced by the CER. To avoid Secret bloat/unintended ownership, consider deriving the expected Secret names from cer.Spec.Phases[].Objects[].Ref and only patching those.

Suggested change
// List Secrets with the revision-name label
secretList := &corev1.SecretList{}
if err := bc.Client.List(ctx, secretList,
client.InNamespace(bc.SystemNamespace),
client.MatchingLabels{labels.RevisionNameKey: cer.Name},
); err != nil {
return fmt.Errorf("listing ref Secrets for revision %q: %w", cer.Name, err)
}
var needsPatch []corev1.Secret
for _, s := range secretList.Items {
hasOwnerRef := false
for _, ref := range s.OwnerReferences {
if ref.UID == cer.UID {
hasOwnerRef = true
break
}
}
if !hasOwnerRef {
needsPatch = append(needsPatch, s)
}
}
if len(needsPatch) == 0 {
return nil
}
return bc.patchOwnerRefsOnSecrets(ctx, cer.Name, cer.UID, needsPatch)
// Derive the set of expected Secret references from the CER spec instead of
// adopting all Secrets with the revision-name label. This avoids
// accidentally adopting stale/orphaned Secrets.
var secretsToPatch []corev1.Secret
for _, phase := range cer.Spec.Phases {
for _, obj := range phase.Objects {
ref := obj.Ref
if ref == nil {
continue
}
// Only consider Secret references.
if ref.Kind != "Secret" {
continue
}
// Default to the system namespace if the reference does not specify one,
// preserving previous behavior that targeted Secrets in bc.SystemNamespace.
ns := ref.Namespace
if ns == "" {
ns = bc.SystemNamespace
}
secret := &corev1.Secret{}
if err := bc.Client.Get(ctx, client.ObjectKey{Namespace: ns, Name: ref.Name}, secret); err != nil {
if apierrors.IsNotFound(err) {
// If the referenced Secret no longer exists, skip it.
continue
}
return fmt.Errorf("getting referenced Secret %s/%s for revision %q: %w", ns, ref.Name, cer.Name, err)
}
secretsToPatch = append(secretsToPatch, *secret)
}
}
if len(secretsToPatch) == 0 {
return nil
}
return bc.patchOwnerRefsOnSecrets(ctx, cer.Name, cer.UID, secretsToPatch)

Copilot uses AI. Check for mistakes.
@codecov
Copy link

codecov bot commented Mar 25, 2026

Codecov Report

❌ Patch coverage is 78.05755% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.13%. Comparing base (d57c077) to head (8d43ef4).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
internal/operator-controller/applier/boxcutter.go 81.61% 14 Missing and 11 partials ⚠️
...ternal/operator-controller/applier/secretpacker.go 79.76% 10 Missing and 7 partials ⚠️
api/v1/zz_generated.deepcopy.go 11.11% 8 Missing ⚠️
...controllers/clusterextensionrevision_controller.go 81.25% 4 Missing and 2 partials ⚠️
...gurations/api/v1/clusterextensionrevisionobject.go 0.00% 3 Missing ⚠️
applyconfigurations/utils.go 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2595      +/-   ##
==========================================
+ Coverage   67.87%   68.13%   +0.25%     
==========================================
  Files         137      139       +2     
  Lines        9588     9850     +262     
==========================================
+ Hits         6508     6711     +203     
- Misses       2583     2622      +39     
- Partials      497      517      +20     
Flag Coverage Δ
e2e 36.88% <0.00%> (-1.16%) ⬇️
experimental-e2e 51.45% <67.39%> (+0.42%) ⬆️
unit 53.37% <68.70%> (+0.42%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Add support for storing ClusterExtensionRevision phase objects in
content-addressable immutable Secrets instead of inline in the CER spec.
This removes the etcd object size limit as a constraint on bundle size.

API changes:
- Add ObjectSourceRef type with name, namespace, and key fields
- Make ClusterExtensionRevisionObject.Object optional (omitzero)
- Add optional Ref field with XValidation ensuring exactly one is set
- Add RefResolutionFailed condition reason
- Add RevisionNameKey label for ref Secret association

Applier (boxcutter.go):
- Add SecretPacker to bin-pack serialized objects into Secrets with
  gzip compression for objects exceeding 800KiB
- Add createExternalizedRevision with crash-safe three-step sequence:
  create Secrets, create CER with refs, patch ownerReferences
- Externalize desiredRevision before SSA comparison so the patch
  compares refs-vs-refs instead of inline-vs-refs
- Add ensureSecretOwnerReferences for crash recovery
- Pass SystemNamespace to Boxcutter from main.go

CER controller:
- Add resolveObjectRef to fetch and decompress objects from Secrets
- Handle ref resolution in buildBoxcutterPhases
- Add RBAC for Secret get/list/watch

E2e tests:
- Add scenario verifying refs, immutability, labels, and ownerRefs
- Add step definitions for ref Secret validation
- Fix listExtensionRevisionResources and
  ClusterExtensionRevisionObjectsNotFoundOrNotOwned to resolve refs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@pedjak pedjak force-pushed the large-bundle-support branch from 606f31b to 8d43ef4 Compare March 25, 2026 19:52
@pedjak pedjak marked this pull request as ready for review March 25, 2026 19:53
Copilot AI review requested due to automatic review settings March 25, 2026 19:53
@openshift-ci openshift-ci bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Mar 25, 2026
@openshift-ci openshift-ci bot requested review from grokspawn and trgeiger March 25, 2026 19:54
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +116 to +126
key := contentHash(data)

// If adding this entry would exceed the limit, finalize the current Secret.
if currentSize+len(data) > maxSecretDataSize && len(currentData) > 0 {
finalizeCurrent()
}

currentData[key] = data
currentSize += len(data)
currentPending = append(currentPending, pendingRef{pos: [2]int{phaseIdx, objIdx}, key: key})
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

SecretPacker.Pack increments currentSize by len(data) even when currentData already contains the same key (e.g., identical objects). Because currentData is a map, the later write overwrites and doesn't actually increase Secret size, but currentSize still grows, which can cause premature splitting into new Secrets or even a false "exceeds maximum" error. Consider only incrementing currentSize when inserting a new key (or track size based on map entries) while still adding pending refs for duplicates so multiple objects can point at the same key.

Copilot uses AI. Check for mistakes.
Comment on lines +533 to +560
secret := &corev1.Secret{}
key := client.ObjectKey{Name: ref.Name, Namespace: ref.Namespace}
if err := c.Client.Get(ctx, key, secret); err != nil {
return nil, fmt.Errorf("getting Secret %s/%s: %w", ref.Namespace, ref.Name, err)
}

data, ok := secret.Data[ref.Key]
if !ok {
return nil, fmt.Errorf("key %q not found in Secret %s/%s", ref.Key, ref.Namespace, ref.Name)
}

// Auto-detect gzip compression (magic bytes 0x1f 0x8b)
if len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b {
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("creating gzip reader for key %q in Secret %s/%s: %w", ref.Key, ref.Namespace, ref.Name, err)
}
defer reader.Close()
decompressed, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("decompressing key %q in Secret %s/%s: %w", ref.Key, ref.Namespace, ref.Name, err)
}
data = decompressed
}

obj := &unstructured.Unstructured{}
if err := json.Unmarshal(data, &obj.Object); err != nil {
return nil, fmt.Errorf("unmarshaling object from key %q in Secret %s/%s: %w", ref.Key, ref.Namespace, ref.Name, err)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

ObjectSourceRef.Namespace is optional in the API/CRD, but resolveObjectRef uses ref.Namespace directly when constructing the Secret key. A CER with a ref that omits namespace will pass validation but then fail reconciliation. Either make namespace required at the API/CRD level, or default an empty ref.Namespace to the configured system namespace (and plumb that config into this reconciler).

Suggested change
secret := &corev1.Secret{}
key := client.ObjectKey{Name: ref.Name, Namespace: ref.Namespace}
if err := c.Client.Get(ctx, key, secret); err != nil {
return nil, fmt.Errorf("getting Secret %s/%s: %w", ref.Namespace, ref.Name, err)
}
data, ok := secret.Data[ref.Key]
if !ok {
return nil, fmt.Errorf("key %q not found in Secret %s/%s", ref.Key, ref.Namespace, ref.Name)
}
// Auto-detect gzip compression (magic bytes 0x1f 0x8b)
if len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b {
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("creating gzip reader for key %q in Secret %s/%s: %w", ref.Key, ref.Namespace, ref.Name, err)
}
defer reader.Close()
decompressed, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("decompressing key %q in Secret %s/%s: %w", ref.Key, ref.Namespace, ref.Name, err)
}
data = decompressed
}
obj := &unstructured.Unstructured{}
if err := json.Unmarshal(data, &obj.Object); err != nil {
return nil, fmt.Errorf("unmarshaling object from key %q in Secret %s/%s: %w", ref.Key, ref.Namespace, ref.Name, err)
ns := ref.Namespace
if ns == "" {
return nil, fmt.Errorf("namespace must be specified in ObjectSourceRef when resolving Secret %q", ref.Name)
}
secret := &corev1.Secret{}
key := client.ObjectKey{Name: ref.Name, Namespace: ns}
if err := c.Client.Get(ctx, key, secret); err != nil {
return nil, fmt.Errorf("getting Secret %s/%s: %w", ns, ref.Name, err)
}
data, ok := secret.Data[ref.Key]
if !ok {
return nil, fmt.Errorf("key %q not found in Secret %s/%s", ref.Key, ns, ref.Name)
}
// Auto-detect gzip compression (magic bytes 0x1f 0x8b)
if len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b {
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("creating gzip reader for key %q in Secret %s/%s: %w", ref.Key, ns, ref.Name, err)
}
defer reader.Close()
decompressed, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("decompressing key %q in Secret %s/%s: %w", ref.Key, ns, ref.Name, err)
}
data = decompressed
}
obj := &unstructured.Unstructured{}
if err := json.Unmarshal(data, &obj.Object); err != nil {
return nil, fmt.Errorf("unmarshaling object from key %q in Secret %s/%s: %w", ref.Key, ns, ref.Name, err)

Copilot uses AI. Check for mistakes.
Comment on lines +544 to +556
// Auto-detect gzip compression (magic bytes 0x1f 0x8b)
if len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b {
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("creating gzip reader for key %q in Secret %s/%s: %w", ref.Key, ref.Namespace, ref.Name, err)
}
defer reader.Close()
decompressed, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("decompressing key %q in Secret %s/%s: %w", ref.Key, ref.Namespace, ref.Name, err)
}
data = decompressed
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

resolveObjectRef uses io.ReadAll on potentially gzip-compressed Secret data without any upper bound on the decompressed size. A small gzip payload can expand massively and cause memory pressure/DoS. Consider enforcing a max decompressed size (e.g., via io.LimitReader) and returning a clear error when the limit is exceeded.

Copilot uses AI. Check for mistakes.
}
objs = append(objs, resolved)
case len(specObj.Object.Object) > 0:
objs = append(objs, &specObj.Object)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

listExtensionRevisionResources silently skips a phase object when neither ref nor inline object is detected (no default/error branch). That can hide invalid CERs and return an incomplete resource list, which could lead to false positives in e2e assertions. Consider returning an error when an object has neither (or when the inline object map is empty) to make failures explicit.

Suggested change
objs = append(objs, &specObj.Object)
objs = append(objs, &specObj.Object)
default:
return nil, fmt.Errorf("phase %q object %d has neither ref nor inline object", phase.Name, j)

Copilot uses AI. Check for mistakes.
//
// +optional
// +kubebuilder:validation:MaxLength=63
Namespace string `json:"namespace,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

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

Does omission of namespace imply OLM system namespace? If so could we add a comment here for that?

// List Secrets with the revision-name label
secretList := &corev1.SecretList{}
if err := bc.Client.List(ctx, secretList,
client.InNamespace(bc.SystemNamespace),
Copy link
Contributor

Choose a reason for hiding this comment

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

I know we're currently just putting all the secrets in the system namespace, but if we have a namespace field on the ObjectSourceRefs in the CER, shouldn't they be used here? Or, do we even need the namespace field in there if they're always in SystemNamespace?

Copy link
Contributor

Choose a reason for hiding this comment

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

To maybe answer my own question: since CER controller doesn't handle Secrets lifecycle, it must assume they exist in the system namespace. Also, upon CER creation the ObjectSourceRef hasn't been filled out yet thus there's no namespace to reference in there. Is that right?

@dtfranz
Copy link
Contributor

dtfranz commented Mar 26, 2026

Not that it really matters at all but, I'd argue this PR is more of a ✨ than a 🌱 PR 🤷
At best it might just make it stand out a bit more in the PR list for potential reviewers but not a blocker issue at all.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants