Skip to content

Commit d7760da

Browse files
(feat): [Boxcutter] Use ClusterExtension ServiceAccount for revision operations
Implement serviceAccount-scoped operations for ClusterExtensionRevision controller as requested. 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 - TrackingCache continues using global client for caching/cleanup - Comprehensive tests for error paths and factory behavior This ensures extension installations respect ServiceAccount RBAC instead of using admin privileges. Assisted-by: Cursor
1 parent dba48b9 commit d7760da

File tree

6 files changed

+427
-46
lines changed

6 files changed

+427
-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
}

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)

internal/operator-controller/controllers/clusterextensionrevision_controller.go

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,15 @@ 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)
4955
}
5056

5157
type trackingCache interface {
@@ -60,6 +66,12 @@ type RevisionEngine interface {
6066
Reconcile(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error)
6167
}
6268

69+
// RevisionEngineFactory creates a RevisionEngine with a given kube client.
70+
// This allows each ClusterExtensionRevision to use a client scoped to its serviceAccount.
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,22 @@ func (c *ClusterExtensionRevisionReconciler) toBoxcutterRevision(ctx context.Con
453481
return r, opts, nil
454482
}
455483

484+
// getScopedClient gets the ServiceAccount configuration from the parent ClusterExtension
485+
// and creates a client scoped to that ServiceAccount.
486+
func (c *ClusterExtensionRevisionReconciler) getScopedClient(ctx context.Context, rev *ocv1.ClusterExtensionRevision) (client.Client, error) {
487+
ownerName := rev.Labels[labels.OwnerNameKey]
488+
if ownerName == "" {
489+
return nil, fmt.Errorf("revision %q is missing owner label", rev.Name)
490+
}
491+
492+
ext := &ocv1.ClusterExtension{}
493+
if err := c.Client.Get(ctx, types.NamespacedName{Name: ownerName}, ext); err != nil {
494+
return nil, fmt.Errorf("failed to get owner ClusterExtension %q: %w", ownerName, err)
495+
}
496+
497+
return c.ScopedClientFactory.CreateScopedClient(ctx, ext.Spec.Namespace, ext.Spec.ServiceAccount.Name)
498+
}
499+
456500
var (
457501
deploymentProbe = &probing.GroupKindSelector{
458502
GroupKind: schema.GroupKind{Group: appsv1.GroupName, Kind: "Deployment"},

0 commit comments

Comments
 (0)