Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion cmd/operator-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (

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

cosClient := &secretFallbackClient{
Client: c.mgr.GetClient(),
apiReader: c.mgr.GetAPIReader(),
systemNamespace: cfg.systemNamespace,
}
if err = (&controllers.ClusterObjectSetReconciler{
Client: c.mgr.GetClient(),
Client: cosClient,
RevisionEngineFactory: revisionEngineFactory,
TrackingCache: trackingCache,
}).SetupWithManager(c.mgr); err != nil {
Expand Down Expand Up @@ -791,3 +797,18 @@ func main() {
os.Exit(1)
}
}

// secretFallbackClient wraps a cached client.Client and falls back to direct
// API reads for Secrets outside the system namespace, where the cache does not watch.
type secretFallbackClient struct {
client.Client
apiReader client.Reader
systemNamespace string
}

func (c *secretFallbackClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
if _, isSecret := obj.(*corev1.Secret); isSecret && key.Namespace != c.systemNamespace {
return c.apiReader.Get(ctx, key, obj, opts...)
}
return c.Client.Get(ctx, key, obj, opts...)
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ rules:
verbs:
- list
- watch
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- apiGroups:
- olm.operatorframework.io
resources:
Expand Down
6 changes: 6 additions & 0 deletions manifests/experimental-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2167,6 +2167,12 @@ rules:
verbs:
- list
- watch
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- apiGroups:
- olm.operatorframework.io
resources:
Expand Down
6 changes: 6 additions & 0 deletions manifests/experimental.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2128,6 +2128,12 @@ rules:
verbs:
- list
- watch
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- apiGroups:
- olm.operatorframework.io
resources:
Expand Down
106 changes: 105 additions & 1 deletion test/e2e/features/revision.feature
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,108 @@ Feature: Install ClusterObjectSet
And resource "pod/test-pod" is installed
And resource "configmap/test-configmap-3" is installed
And ClusterObjectSet "${COS_NAME}" reports Progressing as True with Reason Succeeded
And ClusterObjectSet "${COS_NAME}" reports Available as True with Reason ProbesSucceeded
And ClusterObjectSet "${COS_NAME}" reports Available as True with Reason ProbesSucceeded

Scenario: User can install a ClusterObjectSet with objects stored in Secrets
Given ServiceAccount "olm-sa" with needed permissions is available in test namespace
When resource is applied
"""
apiVersion: v1
kind: Secret
metadata:
name: ${COS_NAME}-ref-secret
namespace: ${TEST_NAMESPACE}
immutable: true
type: olm.operatorframework.io/object-data
stringData:
configmap: |
{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "test-configmap-ref",
"namespace": "${TEST_NAMESPACE}"
},
"data": {
"key": "value"
}
}
deployment: |
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "test-httpd",
"namespace": "${TEST_NAMESPACE}",
"labels": {
"app": "test-httpd"
}
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"app": "test-httpd"
}
},
"template": {
"metadata": {
"labels": {
"app": "test-httpd"
}
},
"spec": {
"containers": [
{
"name": "httpd",
"image": "busybox:1.36",
"imagePullPolicy": "IfNotPresent",
"command": ["httpd"],
"args": ["-f", "-p", "8080"],
"securityContext": {
"runAsNonRoot": true,
"runAsUser": 1000,
"allowPrivilegeEscalation": false,
"capabilities": {
"drop": ["ALL"]
},
"seccompProfile": {
"type": "RuntimeDefault"
}
}
}
]
}
}
}
}
"""
And ClusterObjectSet is applied
"""
apiVersion: olm.operatorframework.io/v1
kind: ClusterObjectSet
metadata:
annotations:
olm.operatorframework.io/service-account-name: olm-sa
olm.operatorframework.io/service-account-namespace: ${TEST_NAMESPACE}
name: ${COS_NAME}
spec:
lifecycleState: Active
collisionProtection: Prevent
phases:
- name: resources
objects:
- ref:
name: ${COS_NAME}-ref-secret
namespace: ${TEST_NAMESPACE}
key: configmap
- ref:
name: ${COS_NAME}-ref-secret
namespace: ${TEST_NAMESPACE}
key: deployment
revision: 1
"""
Then ClusterObjectSet "${COS_NAME}" reports Progressing as True with Reason Succeeded
And ClusterObjectSet "${COS_NAME}" reports Available as True with Reason ProbesSucceeded
And resource "configmap/test-configmap-ref" is installed
And resource "deployment/test-httpd" is installed
13 changes: 9 additions & 4 deletions test/e2e/steps/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ import (
)

type resource struct {
name string
kind string
name string
kind string
namespace string
}

type scenarioContext struct {
Expand Down Expand Up @@ -195,8 +196,12 @@ func ScenarioCleanup(ctx context.Context, _ *godog.Scenario, err error) (context
forDeletion = append(forDeletion, resource{name: sc.namespace, kind: "namespace"})
for _, r := range forDeletion {
go func(res resource) {
if _, err := k8sClient("delete", res.kind, res.name, "--ignore-not-found=true"); err != nil {
logger.Info("Error deleting resource", "name", res.name, "namespace", sc.namespace, "stderr", stderrOutput(err))
args := []string{"delete", res.kind, res.name, "--ignore-not-found=true"}
if res.namespace != "" {
args = append(args, "-n", res.namespace)
}
if _, err := k8sClient(args...); err != nil {
logger.Info("Error deleting resource", "name", res.name, "namespace", res.namespace, "stderr", stderrOutput(err))
}
}(r)
Comment on lines 198 to 206
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Cleanup deletes all resources concurrently. With the PR tracking more resources, it becomes more likely that namespace deletion races with deletions of namespaced resources, creating intermittent cleanup failures/noise. Consider deleting in a deterministic order (e.g., delete tracked resources first, then delete the namespace last) and/or avoiding goroutines here so the namespace isn’t removed while other deletes are still in-flight.

Copilot uses AI. Check for mistakes.
}
Expand Down
10 changes: 10 additions & 0 deletions test/e2e/steps/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,16 @@ func ResourceIsApplied(ctx context.Context, yamlTemplate *godog.DocString) error
sc.clusterExtensionName = res.GetName()
} else if res.GetKind() == "ClusterObjectSet" {
sc.clusterObjectSetName = res.GetName()
} else {
namespace := res.GetNamespace()
if namespace == "" {
namespace = sc.namespace
}
sc.addedResources = append(sc.addedResources, resource{
name: res.GetName(),
kind: strings.ToLower(res.GetKind()),
namespace: namespace,
})
}
return nil
}
Expand Down
Loading