diff --git a/docs/concepts/large-bundle-support.md b/docs/concepts/large-bundle-support.md index cf1c247cdc..325aaacc1c 100644 --- a/docs/concepts/large-bundle-support.md +++ b/docs/concepts/large-bundle-support.md @@ -139,12 +139,19 @@ follow for consistency and safe lifecycle management. Recommended conventions: -1. **Immutability**: Secrets should set `immutable: true`. Because COS phases +1. **Secret type**: Secrets should use the dedicated type + `olm.operatorframework.io/object-data` to distinguish them from user-created + Secrets and enable easy identification. The system always sets this type on + Secrets it creates. The reconciler does not enforce the type when resolving + refs — Secrets with any type are accepted — but producers should set it for + consistency. + +2. **Immutability**: Secrets should set `immutable: true`. Because COS phases are immutable, the content backing a ref should not change after creation. Mutable referenced Secrets are not rejected, but modifying them after the COS is created leads to undefined behavior. -2. **Owner references**: Referenced Secrets should carry an ownerReference to +3. **Owner references**: Referenced Secrets should carry an ownerReference to the COS so that Kubernetes garbage collection removes them when the COS is deleted: ```yaml @@ -159,7 +166,7 @@ Recommended conventions: Secret when the COS is deleted. The reconciler does not delete referenced Secrets itself. -3. **Revision label**: A label identifying the owning revision aids discovery, +4. **Revision label**: A label identifying the owning revision aids discovery, debugging, and bulk cleanup: ``` olm.operatorframework.io/revision-name: @@ -237,6 +244,7 @@ metadata: uid: controller: true immutable: true +type: olm.operatorframework.io/object-data data: service-account: cluster-role: @@ -255,6 +263,7 @@ metadata: uid: controller: true immutable: true +type: olm.operatorframework.io/object-data data: my-crd: --- @@ -272,6 +281,7 @@ metadata: uid: controller: true immutable: true +type: olm.operatorframework.io/object-data data: deployment: ``` @@ -653,7 +663,7 @@ rollout semantics are unchanged. | **Crash safety** | 3-step: Secrets → COS → patch ownerRefs; orphan cleanup via revision label | 2-step: COS → Secrets with ownerRefs; simpler but reconciler may see missing Secrets temporarily | | **Flexibility** | Mixed inline/ref per object within the same phase is possible | All-or-nothing — either all phases inline or all externalized | | **Storage efficiency** | Per-object compression misses cross-object redundancy; potentially more Secrets created in edge cases | Better compression from cross-phase redundancy; fewer Secrets | -| **Resource type** | Secret only | Secret only (with dedicated type) | +| **Resource type** | Secret with dedicated type `olm.operatorframework.io/object-data` | Secret with dedicated type `olm.operatorframework.io/revision-phase-data` | | **Phases structure** | Unchanged — phases array preserved as-is; only individual objects gain a new resolution path | Replaced at the top level — phases field swapped for phasesRef | | **Content addressability** | Content hash as Secret data key — key changes when content changes | Content hash embedded in Secret name — detects changes without fetching contents | diff --git a/internal/operator-controller/applier/secretpacker.go b/internal/operator-controller/applier/secretpacker.go index ac88e5267c..45ae9fdb04 100644 --- a/internal/operator-controller/applier/secretpacker.go +++ b/internal/operator-controller/applier/secretpacker.go @@ -143,6 +143,7 @@ func (p *SecretPacker) newSecret(data map[string][]byte) corev1.Secret { }, }, Immutable: ptr.To(true), + Type: labels.SecretTypeObjectData, Data: data, } } diff --git a/internal/operator-controller/applier/secretpacker_test.go b/internal/operator-controller/applier/secretpacker_test.go index fcd19d1823..9603cbe873 100644 --- a/internal/operator-controller/applier/secretpacker_test.go +++ b/internal/operator-controller/applier/secretpacker_test.go @@ -46,6 +46,7 @@ func TestSecretPacker_Pack(t *testing.T) { assert.True(t, strings.HasPrefix(result.Secrets[0].Name, "my-ext-3-"), "Secret name should be content-addressable with revision prefix") assert.Equal(t, "olmv1-system", result.Secrets[0].Namespace) assert.True(t, *result.Secrets[0].Immutable) + assert.Equal(t, labels.SecretTypeObjectData, result.Secrets[0].Type) assert.Equal(t, "my-ext-3", result.Secrets[0].Labels[labels.RevisionNameKey]) assert.Equal(t, "my-ext", result.Secrets[0].Labels[labels.OwnerNameKey]) diff --git a/internal/operator-controller/labels/labels.go b/internal/operator-controller/labels/labels.go index 27d34e9a80..805c34c3b0 100644 --- a/internal/operator-controller/labels/labels.go +++ b/internal/operator-controller/labels/labels.go @@ -1,6 +1,13 @@ package labels +import corev1 "k8s.io/api/core/v1" + const ( + // SecretTypeObjectData is the custom Secret type used for Secrets that store + // externalized object content referenced by ClusterObjectSet ref entries. + // It distinguishes OLM-managed ref Secrets from user-created Secrets. + SecretTypeObjectData corev1.SecretType = "olm.operatorframework.io/object-data" //nolint:gosec // G101 false positive: this is a Kubernetes Secret type identifier, not a credential + // OwnerKindKey is the label key used to record the kind of the owner // resource responsible for creating or managing a ClusterObjectSet. OwnerKindKey = "olm.operatorframework.io/owner-kind" diff --git a/test/e2e/features/install.feature b/test/e2e/features/install.feature index 0e34383e94..9e5292aad7 100644 --- a/test/e2e/features/install.feature +++ b/test/e2e/features/install.feature @@ -529,6 +529,7 @@ Feature: Install ClusterExtension And ClusterObjectSet "${NAME}-1" ref Secrets are immutable And ClusterObjectSet "${NAME}-1" ref Secrets are labeled with revision and owner And ClusterObjectSet "${NAME}-1" ref Secrets have ownerReference to the revision + And ClusterObjectSet "${NAME}-1" ref Secrets have type "olm.operatorframework.io/object-data" @DeploymentConfig Scenario: deploymentConfig nodeSelector is applied to the operator deployment diff --git a/test/e2e/steps/steps.go b/test/e2e/steps/steps.go index 131d5ccfe1..4a4dac1db1 100644 --- a/test/e2e/steps/steps.go +++ b/test/e2e/steps/steps.go @@ -101,6 +101,7 @@ func RegisterSteps(sc *godog.ScenarioContext) { sc.Step(`^(?i)ClusterObjectSet "([^"]+)" ref Secrets are immutable$`, ClusterObjectSetRefSecretsAreImmutable) sc.Step(`^(?i)ClusterObjectSet "([^"]+)" ref Secrets are labeled with revision and owner$`, ClusterObjectSetRefSecretsLabeled) sc.Step(`^(?i)ClusterObjectSet "([^"]+)" ref Secrets have ownerReference to the revision$`, ClusterObjectSetRefSecretsHaveOwnerRef) + sc.Step(`^(?i)ClusterObjectSet "([^"]+)" ref Secrets have type "([^"]+)"$`, ClusterObjectSetReferredSecretsHaveType) sc.Step(`^(?i)resource "([^"]+)" is installed$`, ResourceAvailable) sc.Step(`^(?i)resource "([^"]+)" is available$`, ResourceAvailable) @@ -875,6 +876,28 @@ func ClusterObjectSetRefSecretsHaveOwnerRef(ctx context.Context, revisionName st return nil } +// ClusterObjectSetReferredSecretsHaveType verifies that all ref Secrets for the named +// ClusterObjectSet have the specified Secret type. +func ClusterObjectSetReferredSecretsHaveType(ctx context.Context, revisionName, expectedType string) error { + sc := scenarioCtx(ctx) + revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), sc) + + secrets, err := listRefSecrets(ctx, revisionName) + if err != nil { + return err + } + if len(secrets) == 0 { + return fmt.Errorf("no ref Secrets found for revision %q", revisionName) + } + + for _, s := range secrets { + if string(s.Type) != expectedType { + return fmt.Errorf("secret %s/%s has type %q, expected %q", s.Namespace, s.Name, s.Type, expectedType) + } + } + return nil +} + // collectRefSecretNames returns the unique set of Secret names referenced by the ClusterObjectSet's phase objects. func collectRefSecretNames(ctx context.Context, revisionName string) ([]string, error) { var names []string