Skip to content

Commit 2c82746

Browse files
(feat): [Boxcutter] Use serviceAccount for ClusterExtensionRevision operations
ClusterExtensionRevision controller now uses the serviceAccount from ClusterExtension spec instead of admin client. This applies objects with proper RBAC permissions via a factory pattern that creates scoped clients per revision. Assisted-by: CLAUDE
1 parent dba48b9 commit 2c82746

File tree

6 files changed

+495
-46
lines changed

6 files changed

+495
-46
lines changed

cmd/operator-controller/main.go

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,7 @@ import (
4242
_ "k8s.io/client-go/plugin/pkg/client/auth"
4343
"k8s.io/klog/v2"
4444
"k8s.io/utils/ptr"
45-
"pkg.package-operator.run/boxcutter/machinery"
4645
"pkg.package-operator.run/boxcutter/managedcache"
47-
"pkg.package-operator.run/boxcutter/ownerhandling"
48-
"pkg.package-operator.run/boxcutter/validation"
4946
ctrl "sigs.k8s.io/controller-runtime"
5047
crcache "sigs.k8s.io/controller-runtime/pkg/cache"
5148
"sigs.k8s.io/controller-runtime/pkg/certwatcher"
@@ -653,21 +650,31 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl
653650
return fmt.Errorf("unable to add tracking cache to manager: %v", err)
654651
}
655652

653+
cerCoreClient, err := corev1client.NewForConfig(c.mgr.GetConfig())
654+
if err != nil {
655+
return fmt.Errorf("unable to create client for ClusterExtensionRevision controller: %w", err)
656+
}
657+
cerTokenGetter := authentication.NewTokenGetter(cerCoreClient, authentication.WithExpirationDuration(1*time.Hour))
658+
659+
revisionEngineFactory := &controllers.DefaultRevisionEngineFactory{
660+
Scheme: c.mgr.GetScheme(),
661+
TrackingCache: trackingCache,
662+
DiscoveryClient: discoveryClient,
663+
RESTMapper: c.mgr.GetRESTMapper(),
664+
FieldOwnerPrefix: fieldOwnerPrefix,
665+
}
666+
667+
scopedClientFactory := &controllers.DefaultScopedClientFactory{
668+
BaseConfig: c.mgr.GetConfig(),
669+
Scheme: c.mgr.GetScheme(),
670+
TokenGetter: cerTokenGetter,
671+
}
672+
656673
if err = (&controllers.ClusterExtensionRevisionReconciler{
657-
Client: c.mgr.GetClient(),
658-
RevisionEngine: machinery.NewRevisionEngine(
659-
machinery.NewPhaseEngine(
660-
machinery.NewObjectEngine(
661-
c.mgr.GetScheme(), trackingCache, c.mgr.GetClient(),
662-
ownerhandling.NewNative(c.mgr.GetScheme()),
663-
machinery.NewComparator(ownerhandling.NewNative(c.mgr.GetScheme()), discoveryClient, c.mgr.GetScheme(), fieldOwnerPrefix),
664-
fieldOwnerPrefix, fieldOwnerPrefix,
665-
),
666-
validation.NewClusterPhaseValidator(c.mgr.GetRESTMapper(), c.mgr.GetClient()),
667-
),
668-
validation.NewRevisionValidator(), c.mgr.GetClient(),
669-
),
670-
TrackingCache: trackingCache,
674+
Client: c.mgr.GetClient(),
675+
RevisionEngineFactory: revisionEngineFactory,
676+
TrackingCache: trackingCache,
677+
ScopedClientFactory: scopedClientFactory,
671678
}).SetupWithManager(c.mgr); err != nil {
672679
return fmt.Errorf("unable to setup ClusterExtensionRevision controller: %w", err)
673680
}
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
# How ServiceAccount Permissions Work with BoxcutterRuntime
2+
3+
!!! note
4+
This feature requires the `BoxcutterRuntime` feature-gate to be enabled.
5+
6+
## What This Does
7+
8+
When BoxcutterRuntime is enabled, OLM v1 uses the ServiceAccount you provide to install extensions. Your extension gets only the permissions you grant to that ServiceAccount.
9+
10+
## Prerequisites
11+
12+
* OLM v1 installed with BoxcutterRuntime enabled (see [Enable the Feature](#enable-the-feature))
13+
* A catalog that is being served
14+
* An existing namespace for your extension
15+
16+
## How It Works
17+
18+
### Two ServiceAccounts (Different Purposes)
19+
20+
**1. Installer ServiceAccount** (you create and provide in ClusterExtension)
21+
- **Purpose:** Install and manage the extension
22+
- **Needs:** Permissions to create Deployments, CRDs, RBAC, etc.
23+
24+
**2. Extension Runtime ServiceAccount** (inside the bundle)
25+
- **Purpose:** Run the extension operator after installation
26+
- **Needs:** Permissions for the operator's business logic
27+
28+
!!! important
29+
The installer ServiceAccount must have **all permissions the runtime ServiceAccount needs** PLUS permission to create that ServiceAccount and its RBAC.
30+
31+
### What Happens During Installation
32+
33+
1. You create a ClusterExtension with a ServiceAccount
34+
2. OLM creates a ClusterExtensionRevision (immutable snapshot of version)
35+
3. ClusterExtensionRevision controller reads the ServiceAccount from parent ClusterExtension
36+
4. Controller installs all resources using that ServiceAccount's permissions
37+
5. If ServiceAccount lacks permission, installation fails immediately with clear error
38+
39+
## Example: Install Extension with Limited Permissions
40+
41+
This example installs an extension that can only manage Deployments and Services in one namespace.
42+
43+
### Step 1: Create namespace
44+
45+
```terminal
46+
kubectl create namespace my-app
47+
```
48+
49+
### Step 2: Create ServiceAccount
50+
51+
```yaml title="Create ServiceAccount"
52+
kubectl apply -f - <<EOF
53+
apiVersion: v1
54+
kind: ServiceAccount
55+
metadata:
56+
name: my-app-installer
57+
namespace: my-app
58+
EOF
59+
```
60+
61+
### Step 3: Grant permissions
62+
63+
```yaml title="Create Role with limited permissions"
64+
kubectl apply -f - <<EOF
65+
apiVersion: rbac.authorization.k8s.io/v1
66+
kind: Role
67+
metadata:
68+
name: my-app-installer-role
69+
namespace: my-app
70+
rules:
71+
- apiGroups: ["apps"]
72+
resources: [deployments]
73+
verbs: [create, get, list, watch, update, patch, delete]
74+
- apiGroups: [""]
75+
resources: [services, configmaps, secrets, serviceaccounts]
76+
verbs: [create, get, list, watch, update, patch, delete]
77+
- apiGroups: [rbac.authorization.k8s.io]
78+
resources: [roles, rolebindings]
79+
verbs: [create, get, list, watch, update, patch, delete]
80+
---
81+
apiVersion: rbac.authorization.k8s.io/v1
82+
kind: RoleBinding
83+
metadata:
84+
name: my-app-installer-binding
85+
namespace: my-app
86+
roleRef:
87+
apiGroup: rbac.authorization.k8s.io
88+
kind: Role
89+
name: my-app-installer-role
90+
subjects:
91+
- kind: ServiceAccount
92+
name: my-app-installer
93+
namespace: my-app
94+
EOF
95+
```
96+
97+
### Step 4: Install extension
98+
99+
```yaml title="Create ClusterExtension"
100+
kubectl apply -f - <<EOF
101+
apiVersion: olm.operatorframework.io/v1
102+
kind: ClusterExtension
103+
metadata:
104+
name: my-app
105+
spec:
106+
namespace: my-app
107+
serviceAccount:
108+
name: my-app-installer
109+
source:
110+
sourceType: Catalog
111+
catalog:
112+
packageName: my-package
113+
EOF
114+
```
115+
116+
### Step 5: Verify installation
117+
118+
```terminal title="Check status"
119+
kubectl get clusterextension my-app
120+
```
121+
122+
Expected output:
123+
```
124+
NAME INSTALLED BUNDLE VERSION INSTALLED PROGRESSING AGE
125+
my-app my-package.v1.0.0 1.0.0 True False 30s
126+
```
127+
128+
### What Gets Installed
129+
130+
The extension can:
131+
- Create Deployments, Services, ConfigMaps in `my-app` namespace
132+
- Create ServiceAccounts and RBAC in `my-app` namespace
133+
134+
The extension cannot:
135+
- Create CRDs (no permission → installation fails)
136+
- Access other namespaces (no permission)
137+
- Create cluster-wide resources (no ClusterRole)
138+
139+
## Example: Upgrade Requires More Permissions
140+
141+
### Initial Installation (v1.0.0)
142+
143+
```yaml title="Extension v1.0.0 installed"
144+
ClusterExtension: my-app
145+
version: "1.0.0"
146+
serviceAccount: my-installer
147+
148+
Role: my-installer-role
149+
rules:
150+
- resources: [deployments, services] # ← Only needs these for v1.0.0
151+
```
152+
153+
### Upgrade to v2.0.0 (needs CRDs)
154+
155+
```yaml title="Request upgrade"
156+
kubectl patch clusterextension my-app --type='json' \
157+
-p='[{"op": "replace", "path": "/spec/source/catalog/version", "value": "2.0.0"}]'
158+
```
159+
160+
**What happens:**
161+
162+
1. OLM creates `ClusterExtensionRevision` `my-app-2` for v2.0.0
163+
2. Controller tries to install using ServiceAccount `my-installer`
164+
3. v2.0.0 bundle contains a CRD
165+
4. Controller tries: `scopedClient.Create(ctx, crd)`
166+
5. **Fails:** ServiceAccount lacks CRD permission
167+
168+
**Error in ClusterExtension status:**
169+
170+
```terminal
171+
kubectl describe clusterextension my-app
172+
```
173+
174+
```
175+
Status:
176+
Conditions:
177+
Type: Progressing
178+
Status: True
179+
Reason: Retrying
180+
Message: permission denied: serviceAccount "my-installer" cannot create
181+
customresourcedefinitions in API group "apiextensions.k8s.io"
182+
```
183+
184+
### Fix: Add Missing Permissions
185+
186+
```yaml title="Add CRD permissions"
187+
kubectl apply -f - <<EOF
188+
apiVersion: rbac.authorization.k8s.io/v1
189+
kind: ClusterRole
190+
metadata:
191+
name: my-installer-cluster-role
192+
rules:
193+
- apiGroups: [apiextensions.k8s.io]
194+
resources: [customresourcedefinitions]
195+
verbs: [create, get, list, watch, update, patch, delete]
196+
---
197+
apiVersion: rbac.authorization.k8s.io/v1
198+
kind: ClusterRoleBinding
199+
metadata:
200+
name: my-installer-cluster-binding
201+
roleRef:
202+
apiGroup: rbac.authorization.k8s.io
203+
kind: ClusterRole
204+
name: my-installer-cluster-role
205+
subjects:
206+
- kind: ServiceAccount
207+
name: my-installer
208+
namespace: my-app
209+
EOF
210+
```
211+
212+
**Controller automatically retries** → upgrade succeeds!
213+
214+
## Enable the Feature
215+
216+
!!! tip
217+
OLM v1 must be installed first. See [getting started](../../getting-started/olmv1_getting_started.md).
218+
219+
```terminal title="Enable BoxcutterRuntime"
220+
kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' \
221+
-p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--feature-gates=BoxcutterRuntime=true"}]'
222+
```
223+
224+
```terminal title="Wait for rollout"
225+
kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager
226+
```
227+
228+
## Troubleshooting
229+
230+
### Extension stuck in Progressing state
231+
232+
```terminal
233+
kubectl describe clusterextension <name>
234+
```
235+
236+
Look for permission errors in the conditions.
237+
238+
### Permission denied during installation
239+
240+
Your ServiceAccount needs more permissions. Check the error message to see what's missing.
241+
242+
See [Derive ServiceAccount](derive-service-account.md) for help finding required permissions.
243+
244+
### Extension won't install
245+
246+
Check:
247+
248+
1. ServiceAccount exists: `kubectl get sa <name> -n <namespace>`
249+
2. Role/RoleBinding exist: `kubectl get role,rolebinding -n <namespace>`
250+
3. Permissions match what the bundle needs
251+
252+
## For Testing: Use cluster-admin
253+
254+
!!! warning
255+
Only for development/testing. Never in production.
256+
257+
Give your ServiceAccount full admin permissions:
258+
259+
```terminal
260+
kubectl create clusterrolebinding my-ext-admin \
261+
--clusterrole=cluster-admin \
262+
--serviceaccount=<namespace>:<serviceaccount-name>
263+
```
264+
265+
This bypasses all permission checks. Use only for quick testing.
266+
267+
## Related Documentation
268+
269+
- [Derive ServiceAccount](derive-service-account.md) - Determine required RBAC permissions
270+
- [Permission Model](../../concepts/permission-model.md) - OLM v1 permission architecture
271+
- [Install Extension](../../tutorials/install-extension.md) - Basic installation guide

internal/operator-controller/applier/boxcutter_test.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ func Test_SimpleRevisionGenerator_GenerateRevisionFromHelmRelease(t *testing.T)
6666
ObjectMeta: metav1.ObjectMeta{
6767
Name: "test-123",
6868
},
69+
Spec: ocv1.ClusterExtensionSpec{
70+
Namespace: "test-namespace",
71+
ServiceAccount: ocv1.ServiceAccountReference{
72+
Name: "test-sa",
73+
},
74+
},
6975
}
7076

7177
objectLabels := map[string]string{
@@ -172,6 +178,12 @@ func Test_SimpleRevisionGenerator_GenerateRevision(t *testing.T) {
172178
ObjectMeta: metav1.ObjectMeta{
173179
Name: "test-extension",
174180
},
181+
Spec: ocv1.ClusterExtensionSpec{
182+
Namespace: "test-namespace",
183+
ServiceAccount: ocv1.ServiceAccountReference{
184+
Name: "test-sa",
185+
},
186+
},
175187
}
176188

177189
rev, err := b.GenerateRevision(t.Context(), fstest.MapFS{}, ext, map[string]string{}, map[string]string{})
@@ -291,7 +303,12 @@ func Test_SimpleRevisionGenerator_AppliesObjectLabelsAndRevisionAnnotations(t *t
291303
"other": "value",
292304
}
293305

294-
rev, err := b.GenerateRevision(t.Context(), fstest.MapFS{}, &ocv1.ClusterExtension{}, map[string]string{
306+
rev, err := b.GenerateRevision(t.Context(), fstest.MapFS{}, &ocv1.ClusterExtension{
307+
Spec: ocv1.ClusterExtensionSpec{
308+
Namespace: "test-namespace",
309+
ServiceAccount: ocv1.ServiceAccountReference{Name: "test-sa"},
310+
},
311+
}, map[string]string{
295312
"some": "value",
296313
}, revAnnotations)
297314
require.NoError(t, err)
@@ -319,7 +336,12 @@ func Test_SimpleRevisionGenerator_Failure(t *testing.T) {
319336
ManifestProvider: r,
320337
}
321338

322-
rev, err := b.GenerateRevision(t.Context(), fstest.MapFS{}, &ocv1.ClusterExtension{}, map[string]string{}, map[string]string{})
339+
rev, err := b.GenerateRevision(t.Context(), fstest.MapFS{}, &ocv1.ClusterExtension{
340+
Spec: ocv1.ClusterExtensionSpec{
341+
Namespace: "test-namespace",
342+
ServiceAccount: ocv1.ServiceAccountReference{Name: "test-sa"},
343+
},
344+
}, map[string]string{}, map[string]string{})
323345
require.Nil(t, rev)
324346
t.Log("by checking rendering errors are propagated")
325347
require.Error(t, err)

0 commit comments

Comments
 (0)