Skip to content

Commit b6ad85e

Browse files
(feat): [Boxcutter] Use ClusterExtension ServiceAccount for revision operations
Implement serviceAccount-scoped operations for ClusterExtensionRevision controller. Changes include: - Add RevisionEngineFactory to create engines with scoped clients - CER controller reads ServiceAccount from parent ClusterExtension - Factory pattern produces RevisionEngine per ServiceAccount - Scoped client enforces RBAC for all resource operations - ServiceAccount stored as annotation for observability - Fallback to impersonation when ServiceAccount not configured - TrackingCache continues using global client for caching/cleanup - Nil check for TokenGetter to prevent panics - Comprehensive tests for error paths and factory behavior This ensures extension installations respect ServiceAccount RBAC instead of using admin privileges. Supports both token-based auth (when SA configured) and impersonation (olm:clusterextensions group) as fallback for simple-install mode. Assisted-by: Cursor
1 parent 5227a12 commit b6ad85e

8 files changed

Lines changed: 459 additions & 60 deletions

File tree

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
}

internal/operator-controller/applier/boxcutter.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@ func (r *SimpleRevisionGenerator) buildClusterExtensionRevision(
186186
ext *ocv1.ClusterExtension,
187187
annotations map[string]string,
188188
) *ocv1.ClusterExtensionRevision {
189+
if annotations == nil {
190+
annotations = make(map[string]string)
191+
}
192+
annotations[labels.ServiceAccountNameKey] = ext.Spec.ServiceAccount.Name
193+
189194
return &ocv1.ClusterExtensionRevision{
190195
ObjectMeta: metav1.ObjectMeta{
191196
Annotations: annotations,

internal/operator-controller/applier/boxcutter_test.go

Lines changed: 29 additions & 6 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{
@@ -79,10 +85,11 @@ func Test_SimpleRevisionGenerator_GenerateRevisionFromHelmRelease(t *testing.T)
7985
ObjectMeta: metav1.ObjectMeta{
8086
Name: "test-123-1",
8187
Annotations: map[string]string{
82-
"olm.operatorframework.io/bundle-name": "my-bundle",
83-
"olm.operatorframework.io/bundle-reference": "bundle-ref",
84-
"olm.operatorframework.io/bundle-version": "1.2.0",
85-
"olm.operatorframework.io/package-name": "my-package",
88+
"olm.operatorframework.io/bundle-name": "my-bundle",
89+
"olm.operatorframework.io/bundle-reference": "bundle-ref",
90+
"olm.operatorframework.io/bundle-version": "1.2.0",
91+
"olm.operatorframework.io/package-name": "my-package",
92+
"olm.operatorframework.io/service-account-name": "test-sa",
8693
},
8794
Labels: map[string]string{
8895
labels.OwnerNameKey: "test-123",
@@ -172,6 +179,12 @@ func Test_SimpleRevisionGenerator_GenerateRevision(t *testing.T) {
172179
ObjectMeta: metav1.ObjectMeta{
173180
Name: "test-extension",
174181
},
182+
Spec: ocv1.ClusterExtensionSpec{
183+
Namespace: "test-namespace",
184+
ServiceAccount: ocv1.ServiceAccountReference{
185+
Name: "test-sa",
186+
},
187+
},
175188
}
176189

177190
rev, err := b.GenerateRevision(t.Context(), fstest.MapFS{}, ext, map[string]string{}, map[string]string{})
@@ -291,7 +304,12 @@ func Test_SimpleRevisionGenerator_AppliesObjectLabelsAndRevisionAnnotations(t *t
291304
"other": "value",
292305
}
293306

294-
rev, err := b.GenerateRevision(t.Context(), fstest.MapFS{}, &ocv1.ClusterExtension{}, map[string]string{
307+
rev, err := b.GenerateRevision(t.Context(), fstest.MapFS{}, &ocv1.ClusterExtension{
308+
Spec: ocv1.ClusterExtensionSpec{
309+
Namespace: "test-namespace",
310+
ServiceAccount: ocv1.ServiceAccountReference{Name: "test-sa"},
311+
},
312+
}, map[string]string{
295313
"some": "value",
296314
}, revAnnotations)
297315
require.NoError(t, err)
@@ -319,7 +337,12 @@ func Test_SimpleRevisionGenerator_Failure(t *testing.T) {
319337
ManifestProvider: r,
320338
}
321339

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

internal/operator-controller/controllers/clusterextensionrevision_controller.go

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,16 @@ const (
4343
// ClusterExtensionRevisionReconciler actions individual snapshots of ClusterExtensions,
4444
// as part of the boxcutter integration.
4545
type ClusterExtensionRevisionReconciler struct {
46-
Client client.Client
47-
RevisionEngine RevisionEngine
48-
TrackingCache trackingCache
46+
Client client.Client
47+
RevisionEngineFactory RevisionEngineFactory
48+
TrackingCache trackingCache
49+
ScopedClientFactory ScopedClientFactory
50+
}
51+
52+
// ScopedClientFactory creates a client scoped to a specific ServiceAccount.
53+
type ScopedClientFactory interface {
54+
CreateScopedClient(ctx context.Context, namespace, serviceAccountName string) (client.Client, error)
55+
CreateImpersonatedClient(ctx context.Context, ext *ocv1.ClusterExtension) (client.Client, error)
4956
}
5057

5158
type trackingCache interface {
@@ -60,6 +67,11 @@ type RevisionEngine interface {
6067
Reconcile(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error)
6168
}
6269

70+
// RevisionEngineFactory creates a RevisionEngine with a given kube client.
71+
type RevisionEngineFactory interface {
72+
CreateRevisionEngine(client client.Client) RevisionEngine
73+
}
74+
6375
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensionrevisions,verbs=get;list;watch;update;patch;create;delete
6476
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensionrevisions/status,verbs=update;patch
6577
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensionrevisions/finalizers,verbs=update
@@ -139,7 +151,15 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
139151
return ctrl.Result{}, werr
140152
}
141153

142-
rres, err := c.RevisionEngine.Reconcile(ctx, *revision, opts...)
154+
scopedClient, err := c.getScopedClient(ctx, rev)
155+
if err != nil {
156+
setRetryingConditions(rev, err.Error())
157+
return ctrl.Result{}, fmt.Errorf("failed to create client with serviceAccount permissions: %v", err)
158+
}
159+
160+
revisionEngine := c.RevisionEngineFactory.CreateRevisionEngine(scopedClient)
161+
162+
rres, err := revisionEngine.Reconcile(ctx, *revision, opts...)
143163
if err != nil {
144164
if rres != nil {
145165
// Log detailed reconcile reports only in debug mode (V(1)) to reduce verbosity.
@@ -253,7 +273,15 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
253273
func (c *ClusterExtensionRevisionReconciler) teardown(ctx context.Context, rev *ocv1.ClusterExtensionRevision, revision *boxcutter.Revision) (ctrl.Result, error) {
254274
l := log.FromContext(ctx)
255275

256-
tres, err := c.RevisionEngine.Teardown(ctx, *revision)
276+
scopedClient, err := c.getScopedClient(ctx, rev)
277+
if err != nil {
278+
markAsAvailableUnknown(rev, ocv1.ClusterExtensionRevisionReasonReconciling, err.Error())
279+
return ctrl.Result{}, fmt.Errorf("failed to create client for teardown: %v", err)
280+
}
281+
282+
revisionEngine := c.RevisionEngineFactory.CreateRevisionEngine(scopedClient)
283+
284+
tres, err := revisionEngine.Teardown(ctx, *revision)
257285
if err != nil {
258286
if tres != nil {
259287
l.V(1).Info("teardown failure report", "report", tres.String())
@@ -453,6 +481,30 @@ func (c *ClusterExtensionRevisionReconciler) toBoxcutterRevision(ctx context.Con
453481
return r, opts, nil
454482
}
455483

484+
// getScopedClient gets the ServiceAccount from the parent ClusterExtension
485+
// and creates a client scoped to that ServiceAccount. If ServiceAccount is not
486+
// configured, it falls back to impersonation using synthetic users.
487+
func (c *ClusterExtensionRevisionReconciler) getScopedClient(ctx context.Context, rev *ocv1.ClusterExtensionRevision) (client.Client, error) {
488+
ownerName := rev.Labels[labels.OwnerNameKey]
489+
if ownerName == "" {
490+
return nil, fmt.Errorf("revision %q is missing owner label", rev.Name)
491+
}
492+
493+
ext := &ocv1.ClusterExtension{}
494+
if err := c.Client.Get(ctx, types.NamespacedName{Name: ownerName}, ext); err != nil {
495+
return nil, fmt.Errorf("failed to get owner ClusterExtension %q: %w", ownerName, err)
496+
}
497+
498+
// ServiceAccount is currently required by API validation, but handle optional case
499+
// for future flexibility or when validation might be relaxed
500+
if ext.Spec.ServiceAccount.Name == "" {
501+
// Fallback to impersonation if ServiceAccount not configured
502+
return c.ScopedClientFactory.CreateImpersonatedClient(ctx, ext)
503+
}
504+
505+
return c.ScopedClientFactory.CreateScopedClient(ctx, ext.Spec.Namespace, ext.Spec.ServiceAccount.Name)
506+
}
507+
456508
var (
457509
deploymentProbe = &probing.GroupKindSelector{
458510
GroupKind: schema.GroupKind{Group: appsv1.GroupName, Kind: "Deployment"},

0 commit comments

Comments
 (0)