Skip to content

Commit a6989b7

Browse files
pedjakclaude
andauthored
Fix ClusterObjectSet ref resolution for Secrets outside system namespace (#2624)
The controller cache only watches the system namespace, causing ref resolution to fail when Secrets are stored in other namespaces. Fix by introducing a client wrapper that falls back to direct API calls for Secret reads outside the system namespace, and grant cluster-wide Secret get permission when BoxcutterRuntime is enabled. Adds an e2e scenario covering this path. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 735b41e commit a6989b7

File tree

7 files changed

+164
-6
lines changed

7 files changed

+164
-6
lines changed

cmd/operator-controller/main.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030

3131
"github.com/spf13/cobra"
3232
"go.podman.io/image/v5/types"
33+
corev1 "k8s.io/api/core/v1"
3334
rbacv1 "k8s.io/api/rbac/v1"
3435
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
3536
apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
@@ -697,8 +698,13 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl
697698
return fmt.Errorf("unable to create revision engine factory: %w", err)
698699
}
699700

701+
cosClient := &secretFallbackClient{
702+
Client: c.mgr.GetClient(),
703+
apiReader: c.mgr.GetAPIReader(),
704+
systemNamespace: cfg.systemNamespace,
705+
}
700706
if err = (&controllers.ClusterObjectSetReconciler{
701-
Client: c.mgr.GetClient(),
707+
Client: cosClient,
702708
RevisionEngineFactory: revisionEngineFactory,
703709
TrackingCache: trackingCache,
704710
}).SetupWithManager(c.mgr); err != nil {
@@ -791,3 +797,18 @@ func main() {
791797
os.Exit(1)
792798
}
793799
}
800+
801+
// secretFallbackClient wraps a cached client.Client and falls back to direct
802+
// API reads for Secrets outside the system namespace, where the cache does not watch.
803+
type secretFallbackClient struct {
804+
client.Client
805+
apiReader client.Reader
806+
systemNamespace string
807+
}
808+
809+
func (c *secretFallbackClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
810+
if _, isSecret := obj.(*corev1.Secret); isSecret && key.Namespace != c.systemNamespace {
811+
return c.apiReader.Get(ctx, key, obj, opts...)
812+
}
813+
return c.Client.Get(ctx, key, obj, opts...)
814+
}

helm/olmv1/templates/rbac/clusterrole-operator-controller-manager-role.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ rules:
8686
verbs:
8787
- list
8888
- watch
89+
- apiGroups:
90+
- ""
91+
resources:
92+
- secrets
93+
verbs:
94+
- get
8995
- apiGroups:
9096
- olm.operatorframework.io
9197
resources:

manifests/experimental-e2e.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2167,6 +2167,12 @@ rules:
21672167
verbs:
21682168
- list
21692169
- watch
2170+
- apiGroups:
2171+
- ""
2172+
resources:
2173+
- secrets
2174+
verbs:
2175+
- get
21702176
- apiGroups:
21712177
- olm.operatorframework.io
21722178
resources:

manifests/experimental.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2128,6 +2128,12 @@ rules:
21282128
verbs:
21292129
- list
21302130
- watch
2131+
- apiGroups:
2132+
- ""
2133+
resources:
2134+
- secrets
2135+
verbs:
2136+
- get
21312137
- apiGroups:
21322138
- olm.operatorframework.io
21332139
resources:

test/e2e/features/revision.feature

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,4 +338,108 @@ Feature: Install ClusterObjectSet
338338
And resource "pod/test-pod" is installed
339339
And resource "configmap/test-configmap-3" is installed
340340
And ClusterObjectSet "${COS_NAME}" reports Progressing as True with Reason Succeeded
341-
And ClusterObjectSet "${COS_NAME}" reports Available as True with Reason ProbesSucceeded
341+
And ClusterObjectSet "${COS_NAME}" reports Available as True with Reason ProbesSucceeded
342+
343+
Scenario: User can install a ClusterObjectSet with objects stored in Secrets
344+
Given ServiceAccount "olm-sa" with needed permissions is available in test namespace
345+
When resource is applied
346+
"""
347+
apiVersion: v1
348+
kind: Secret
349+
metadata:
350+
name: ${COS_NAME}-ref-secret
351+
namespace: ${TEST_NAMESPACE}
352+
immutable: true
353+
type: olm.operatorframework.io/object-data
354+
stringData:
355+
configmap: |
356+
{
357+
"apiVersion": "v1",
358+
"kind": "ConfigMap",
359+
"metadata": {
360+
"name": "test-configmap-ref",
361+
"namespace": "${TEST_NAMESPACE}"
362+
},
363+
"data": {
364+
"key": "value"
365+
}
366+
}
367+
deployment: |
368+
{
369+
"apiVersion": "apps/v1",
370+
"kind": "Deployment",
371+
"metadata": {
372+
"name": "test-httpd",
373+
"namespace": "${TEST_NAMESPACE}",
374+
"labels": {
375+
"app": "test-httpd"
376+
}
377+
},
378+
"spec": {
379+
"replicas": 1,
380+
"selector": {
381+
"matchLabels": {
382+
"app": "test-httpd"
383+
}
384+
},
385+
"template": {
386+
"metadata": {
387+
"labels": {
388+
"app": "test-httpd"
389+
}
390+
},
391+
"spec": {
392+
"containers": [
393+
{
394+
"name": "httpd",
395+
"image": "busybox:1.36",
396+
"imagePullPolicy": "IfNotPresent",
397+
"command": ["httpd"],
398+
"args": ["-f", "-p", "8080"],
399+
"securityContext": {
400+
"runAsNonRoot": true,
401+
"runAsUser": 1000,
402+
"allowPrivilegeEscalation": false,
403+
"capabilities": {
404+
"drop": ["ALL"]
405+
},
406+
"seccompProfile": {
407+
"type": "RuntimeDefault"
408+
}
409+
}
410+
}
411+
]
412+
}
413+
}
414+
}
415+
}
416+
"""
417+
And ClusterObjectSet is applied
418+
"""
419+
apiVersion: olm.operatorframework.io/v1
420+
kind: ClusterObjectSet
421+
metadata:
422+
annotations:
423+
olm.operatorframework.io/service-account-name: olm-sa
424+
olm.operatorframework.io/service-account-namespace: ${TEST_NAMESPACE}
425+
name: ${COS_NAME}
426+
spec:
427+
lifecycleState: Active
428+
collisionProtection: Prevent
429+
phases:
430+
- name: resources
431+
objects:
432+
- ref:
433+
name: ${COS_NAME}-ref-secret
434+
namespace: ${TEST_NAMESPACE}
435+
key: configmap
436+
- ref:
437+
name: ${COS_NAME}-ref-secret
438+
namespace: ${TEST_NAMESPACE}
439+
key: deployment
440+
revision: 1
441+
"""
442+
Then ClusterObjectSet "${COS_NAME}" reports Progressing as True with Reason Succeeded
443+
And ClusterObjectSet "${COS_NAME}" reports Available as True with Reason ProbesSucceeded
444+
And resource "configmap/test-configmap-ref" is installed
445+
And resource "deployment/test-httpd" is installed

test/e2e/steps/hooks.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ import (
2222
)
2323

2424
type resource struct {
25-
name string
26-
kind string
25+
name string
26+
kind string
27+
namespace string
2728
}
2829

2930
type scenarioContext struct {
@@ -195,8 +196,12 @@ func ScenarioCleanup(ctx context.Context, _ *godog.Scenario, err error) (context
195196
forDeletion = append(forDeletion, resource{name: sc.namespace, kind: "namespace"})
196197
for _, r := range forDeletion {
197198
go func(res resource) {
198-
if _, err := k8sClient("delete", res.kind, res.name, "--ignore-not-found=true"); err != nil {
199-
logger.Info("Error deleting resource", "name", res.name, "namespace", sc.namespace, "stderr", stderrOutput(err))
199+
args := []string{"delete", res.kind, res.name, "--ignore-not-found=true"}
200+
if res.namespace != "" {
201+
args = append(args, "-n", res.namespace)
202+
}
203+
if _, err := k8sClient(args...); err != nil {
204+
logger.Info("Error deleting resource", "name", res.name, "namespace", res.namespace, "stderr", stderrOutput(err))
200205
}
201206
}(r)
202207
}

test/e2e/steps/steps.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,16 @@ func ResourceIsApplied(ctx context.Context, yamlTemplate *godog.DocString) error
310310
sc.clusterExtensionName = res.GetName()
311311
} else if res.GetKind() == "ClusterObjectSet" {
312312
sc.clusterObjectSetName = res.GetName()
313+
} else {
314+
namespace := res.GetNamespace()
315+
if namespace == "" {
316+
namespace = sc.namespace
317+
}
318+
sc.addedResources = append(sc.addedResources, resource{
319+
name: res.GetName(),
320+
kind: strings.ToLower(res.GetKind()),
321+
namespace: namespace,
322+
})
313323
}
314324
return nil
315325
}

0 commit comments

Comments
 (0)