Skip to content

Commit 898516d

Browse files
pedjakclaude
andcommitted
Address PR #2595 review feedback
- Fix duplicate key size inflation in SecretPacker by only incrementing size for new content hash keys - Add io.LimitReader (10 MiB cap) for gzip decompression to prevent gzip bombs in controller and e2e helpers - Add doc comment clarifying ObjectSourceRef.Namespace defaults to OLM system namespace during ref resolution - Fix docs: orphan cleanup uses ownerReference GC, ref resolution failures are retried (not terminal) - Remove unused ClusterExtensionRevisionReasonRefResolutionFailed constant - Add default error branch in e2e listExtensionRevisionResources for objects missing both ref and inline content Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 69944aa commit 898516d

10 files changed

Lines changed: 70 additions & 25 deletions

File tree

api/v1/clusterextensionrevision_types.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,12 @@ const (
3030
ClusterExtensionRevisionTypeSucceeded = "Succeeded"
3131

3232
// Condition Reasons
33-
ClusterExtensionRevisionReasonArchived = "Archived"
34-
ClusterExtensionRevisionReasonBlocked = "Blocked"
35-
ClusterExtensionRevisionReasonProbeFailure = "ProbeFailure"
36-
ClusterExtensionRevisionReasonProbesSucceeded = "ProbesSucceeded"
37-
ClusterExtensionRevisionReasonReconciling = "Reconciling"
38-
ClusterExtensionRevisionReasonRefResolutionFailed = "RefResolutionFailed"
39-
ClusterExtensionRevisionReasonRetrying = "Retrying"
33+
ClusterExtensionRevisionReasonArchived = "Archived"
34+
ClusterExtensionRevisionReasonBlocked = "Blocked"
35+
ClusterExtensionRevisionReasonProbeFailure = "ProbeFailure"
36+
ClusterExtensionRevisionReasonProbesSucceeded = "ProbesSucceeded"
37+
ClusterExtensionRevisionReasonReconciling = "Reconciling"
38+
ClusterExtensionRevisionReasonRetrying = "Retrying"
4039
)
4140

4241
// ClusterExtensionRevisionSpec defines the desired state of ClusterExtensionRevision.
@@ -452,6 +451,7 @@ type ObjectSourceRef struct {
452451
Name string `json:"name"`
453452

454453
// namespace is the namespace of the referenced Secret.
454+
// When empty, defaults to the OLM system namespace during ref resolution.
455455
//
456456
// +optional
457457
// +kubebuilder:validation:MaxLength=63

applyconfigurations/api/v1/objectsourceref.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/concepts/large-bundle-support.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,12 @@ Key properties:
364364
- **No reconciler churn**: Referenced Secrets exist before the CER is created.
365365
The CER reconciler never encounters missing Secrets during normal operation.
366366
- **Orphan cleanup**: Secrets created in step 1 carry the revision label
367-
(`olm.operatorframework.io/revision-name`). If a crash leaves Secrets without
368-
a corresponding CER, the ClusterExtension controller detects and deletes them
369-
on its next reconciliation.
367+
(`olm.operatorframework.io/revision-name`). Once the CER is created in step 2
368+
and ownerReferences are patched in step 3, Kubernetes garbage collection
369+
automatically removes the Secrets when the CER is deleted. If a crash occurs
370+
between steps 1 and 2, the ClusterExtension controller detects orphaned
371+
Secrets (those with the revision label but no corresponding CER) and deletes
372+
them on its next reconciliation.
370373
- **Idempotent retry**: Secrets are immutable in data. Re-creation of an
371374
existing Secret returns AlreadyExists and is skipped. ownerReference patching
372375
is idempotent — patching an already-set ownerReference is a no-op.
@@ -389,7 +392,9 @@ Under normal operation, referenced Secrets are guaranteed to exist before the
389392
CER is created (see [Crash-safe creation sequence](#crash-safe-creation-sequence)).
390393
If a referenced Secret or key is not found — indicating an inconsistent state
391394
caused by external modification or a partially completed creation sequence —
392-
the reconciler sets a terminal error condition on the CER.
395+
the reconciler returns a retryable error, allowing the controller to retry on
396+
subsequent reconciliation attempts. This handles transient issues such as
397+
informer cache lag after Secret creation.
393398
394399
Secrets are fetched using the typed client served from the informer cache.
395400

helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,9 @@ spec:
221221
minLength: 1
222222
type: string
223223
namespace:
224-
description: namespace is the namespace of the referenced
225-
Secret.
224+
description: |-
225+
namespace is the namespace of the referenced Secret.
226+
When empty, defaults to the OLM system namespace during ref resolution.
226227
maxLength: 63
227228
type: string
228229
required:

internal/operator-controller/applier/secretpacker.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,15 @@ func (p *SecretPacker) Pack(phases []ocv1.ClusterExtensionRevisionPhase) (*PackR
115115

116116
key := contentHash(data)
117117

118-
// If adding this entry would exceed the limit, finalize the current Secret.
119-
if currentSize+len(data) > maxSecretDataSize && len(currentData) > 0 {
120-
finalizeCurrent()
118+
// Only add data and increment size for new keys. Duplicate content
119+
// (same hash) reuses the existing entry without inflating the size.
120+
if _, exists := currentData[key]; !exists {
121+
if currentSize+len(data) > maxSecretDataSize && len(currentData) > 0 {
122+
finalizeCurrent()
123+
}
124+
currentData[key] = data
125+
currentSize += len(data)
121126
}
122-
123-
currentData[key] = data
124-
currentSize += len(data)
125127
currentPending = append(currentPending, pendingRef{pos: [2]int{phaseIdx, objIdx}, key: key})
126128
}
127129
}

internal/operator-controller/applier/secretpacker_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,28 @@ func TestSecretPacker_Pack(t *testing.T) {
171171
defer reader.Close()
172172
})
173173

174+
t.Run("duplicate content objects share key and do not inflate size", func(t *testing.T) {
175+
// Two identical objects should produce the same content hash key.
176+
// The second occurrence must not double-count the size.
177+
phases := []ocv1.ClusterExtensionRevisionPhase{{
178+
Name: "deploy",
179+
Objects: []ocv1.ClusterExtensionRevisionObject{
180+
{Object: testConfigMap("same-cm", "default")},
181+
{Object: testConfigMap("same-cm", "default")},
182+
},
183+
}}
184+
185+
result, err := packer.Pack(phases)
186+
require.NoError(t, err)
187+
require.Len(t, result.Secrets, 1)
188+
// Only one data entry despite two objects.
189+
assert.Len(t, result.Secrets[0].Data, 1)
190+
// Both positions get refs.
191+
assert.Len(t, result.Refs, 2)
192+
// Both refs point to the same key.
193+
assert.Equal(t, result.Refs[[2]int{0, 0}].Key, result.Refs[[2]int{0, 1}].Key)
194+
})
195+
174196
t.Run("key is SHA-256 base64url", func(t *testing.T) {
175197
obj := testConfigMap("test-cm", "default")
176198
rawData, err := json.Marshal(obj.Object)

internal/operator-controller/controllers/clusterextensionrevision_controller.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,10 +554,15 @@ func (c *ClusterExtensionRevisionReconciler) resolveObjectRef(ctx context.Contex
554554
return nil, fmt.Errorf("creating gzip reader for key %q in Secret %s/%s: %w", ref.Key, ref.Namespace, ref.Name, err)
555555
}
556556
defer reader.Close()
557-
decompressed, err := io.ReadAll(reader)
557+
const maxDecompressedSize = 10 * 1024 * 1024 // 10 MiB
558+
limited := io.LimitReader(reader, maxDecompressedSize+1)
559+
decompressed, err := io.ReadAll(limited)
558560
if err != nil {
559561
return nil, fmt.Errorf("decompressing key %q in Secret %s/%s: %w", ref.Key, ref.Namespace, ref.Name, err)
560562
}
563+
if len(decompressed) > maxDecompressedSize {
564+
return nil, fmt.Errorf("decompressed data for key %q in Secret %s/%s exceeds maximum size (%d bytes)", ref.Key, ref.Namespace, ref.Name, maxDecompressedSize)
565+
}
561566
data = decompressed
562567
}
563568

manifests/experimental-e2e.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -833,8 +833,9 @@ spec:
833833
minLength: 1
834834
type: string
835835
namespace:
836-
description: namespace is the namespace of the referenced
837-
Secret.
836+
description: |-
837+
namespace is the namespace of the referenced Secret.
838+
When empty, defaults to the OLM system namespace during ref resolution.
838839
maxLength: 63
839840
type: string
840841
required:

manifests/experimental.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -794,8 +794,9 @@ spec:
794794
minLength: 1
795795
type: string
796796
namespace:
797-
description: namespace is the namespace of the referenced
798-
Secret.
797+
description: |-
798+
namespace is the namespace of the referenced Secret.
799+
When empty, defaults to the OLM system namespace during ref resolution.
799800
maxLength: 63
800801
type: string
801802
required:

test/e2e/steps/steps.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1577,6 +1577,8 @@ func listExtensionRevisionResources(extName string) ([]client.Object, error) {
15771577
objs = append(objs, resolved)
15781578
case len(specObj.Object.Object) > 0:
15791579
objs = append(objs, &specObj.Object)
1580+
default:
1581+
return nil, fmt.Errorf("object %d in phase %q has neither object nor ref", j, phase.Name)
15801582
}
15811583
}
15821584
}
@@ -1605,10 +1607,15 @@ func resolveObjectRef(ref ocv1.ObjectSourceRef) (*unstructured.Unstructured, err
16051607
return nil, fmt.Errorf("creating gzip reader for key %q in Secret %s/%s: %w", ref.Key, ref.Namespace, ref.Name, err)
16061608
}
16071609
defer reader.Close()
1608-
decompressed, err := io.ReadAll(reader)
1610+
const maxDecompressedSize = 10 * 1024 * 1024 // 10 MiB
1611+
limited := io.LimitReader(reader, maxDecompressedSize+1)
1612+
decompressed, err := io.ReadAll(limited)
16091613
if err != nil {
16101614
return nil, fmt.Errorf("decompressing key %q in Secret %s/%s: %w", ref.Key, ref.Namespace, ref.Name, err)
16111615
}
1616+
if len(decompressed) > maxDecompressedSize {
1617+
return nil, fmt.Errorf("decompressed data for key %q in Secret %s/%s exceeds maximum size (%d bytes)", ref.Key, ref.Namespace, ref.Name, maxDecompressedSize)
1618+
}
16121619
data = decompressed
16131620
}
16141621
obj := &unstructured.Unstructured{}

0 commit comments

Comments
 (0)