@@ -12,13 +12,15 @@ import (
1212 "fmt"
1313 "net/http"
1414 "strings"
15+ "sync"
1516
1617 "k8s.io/apimachinery/pkg/api/meta"
1718 "k8s.io/apimachinery/pkg/runtime"
1819 "k8s.io/apimachinery/pkg/types"
1920 "k8s.io/client-go/discovery"
2021 "k8s.io/client-go/rest"
2122 "pkg.package-operator.run/boxcutter/machinery"
23+ machinerytypes "pkg.package-operator.run/boxcutter/machinery/types"
2224 "pkg.package-operator.run/boxcutter/managedcache"
2325 "pkg.package-operator.run/boxcutter/ownerhandling"
2426 "pkg.package-operator.run/boxcutter/validation"
@@ -29,26 +31,50 @@ import (
2931 "github.com/operator-framework/operator-controller/internal/operator-controller/labels"
3032)
3133
32- // DefaultRevisionEngineFactory creates boxcutter RevisionEngines with serviceAccount-scoped clients.
33- type DefaultRevisionEngineFactory struct {
34+ // RevisionEngine defines the interface for reconciling and tearing down revisions.
35+ type RevisionEngine interface {
36+ Teardown (ctx context.Context , rev machinerytypes.Revision , opts ... machinerytypes.RevisionTeardownOption ) (machinery.RevisionTeardownResult , error )
37+ Reconcile (ctx context.Context , rev machinerytypes.Revision , opts ... machinerytypes.RevisionReconcileOption ) (machinery.RevisionResult , error )
38+ }
39+
40+ // RevisionEngineFactory creates a RevisionEngine for a ClusterExtensionRevision.
41+ type RevisionEngineFactory interface {
42+ CreateRevisionEngine (ctx context.Context , rev * ocv1.ClusterExtensionRevision ) (RevisionEngine , error )
43+ }
44+
45+ // defaultRevisionEngineFactory creates boxcutter RevisionEngines with serviceAccount-scoped clients.
46+ type defaultRevisionEngineFactory struct {
3447 Scheme * runtime.Scheme
3548 TrackingCache managedcache.TrackingCache
3649 DiscoveryClient discovery.CachedDiscoveryInterface
3750 RESTMapper meta.RESTMapper
3851 FieldOwnerPrefix string
3952 BaseConfig * rest.Config
4053 TokenGetter * authentication.TokenGetter
54+
55+ // engineCache caches RevisionEngines keyed by "namespace/serviceAccountName"
56+ engineCache sync.Map
4157}
4258
4359// CreateRevisionEngine constructs a boxcutter RevisionEngine for the given ClusterExtensionRevision.
44- // It reads the ServiceAccount from annotations and creates a scoped client .
45- func (f * DefaultRevisionEngineFactory ) CreateRevisionEngine (_ context.Context , rev * ocv1.ClusterExtensionRevision ) (RevisionEngine , error ) {
46- scopedClient , err := f .getScopedClient (rev )
60+ // It reads the ServiceAccount from annotations and returns a cached or new engine .
61+ func (f * defaultRevisionEngineFactory ) CreateRevisionEngine (_ context.Context , rev * ocv1.ClusterExtensionRevision ) (RevisionEngine , error ) {
62+ cacheKey , err := f .getServiceAccountKey (rev )
4763 if err != nil {
4864 return nil , err
4965 }
5066
51- return machinery .NewRevisionEngine (
67+ // Check cache first
68+ if cached , ok := f .engineCache .Load (cacheKey ); ok {
69+ return cached .(RevisionEngine ), nil
70+ }
71+
72+ scopedClient , err := f .createScopedClient (cacheKey )
73+ if err != nil {
74+ return nil , err
75+ }
76+
77+ engine := machinery .NewRevisionEngine (
5278 machinery .NewPhaseEngine (
5379 machinery .NewObjectEngine (
5480 f .Scheme , f .TrackingCache , scopedClient ,
@@ -59,35 +85,36 @@ func (f *DefaultRevisionEngineFactory) CreateRevisionEngine(_ context.Context, r
5985 validation .NewClusterPhaseValidator (f .RESTMapper , scopedClient ),
6086 ),
6187 validation .NewRevisionValidator (), scopedClient ,
62- ), nil
88+ )
89+
90+ // Store in cache (LoadOrStore handles race conditions)
91+ actual , _ := f .engineCache .LoadOrStore (cacheKey , engine )
92+ return actual .(RevisionEngine ), nil
6393}
6494
65- func (f * DefaultRevisionEngineFactory ) getScopedClient (rev * ocv1.ClusterExtensionRevision ) (client. Client , error ) {
95+ func (f * defaultRevisionEngineFactory ) getServiceAccountKey (rev * ocv1.ClusterExtensionRevision ) (string , error ) {
6696 annotations := rev .GetAnnotations ()
6797 if annotations == nil {
68- return nil , fmt .Errorf ("revision %q is missing required annotations" , rev .Name )
98+ return "" , fmt .Errorf ("revision %q is missing required annotations" , rev .Name )
6999 }
70100
71101 saName := strings .TrimSpace (annotations [labels .ServiceAccountNameKey ])
72102 saNamespace := strings .TrimSpace (annotations [labels .ServiceAccountNamespaceKey ])
73103
74104 if len (saName ) == 0 {
75- return nil , fmt .Errorf ("revision %q is missing ServiceAccount name annotation" , rev .Name )
105+ return "" , fmt .Errorf ("revision %q is missing ServiceAccount name annotation" , rev .Name )
76106 }
77107 if len (saNamespace ) == 0 {
78- return nil , fmt .Errorf ("revision %q is missing ServiceAccount namespace annotation" , rev .Name )
108+ return "" , fmt .Errorf ("revision %q is missing ServiceAccount namespace annotation" , rev .Name )
79109 }
80110
81- return f . createScopedClient ( saNamespace , saName )
111+ return saNamespace + "/" + saName , nil
82112}
83113
84- func (f * DefaultRevisionEngineFactory ) createScopedClient (namespace , serviceAccountName string ) (client.Client , error ) {
85- if f .TokenGetter == nil {
86- return nil , fmt .Errorf ("TokenGetter is required but not configured" )
87- }
88- if f .BaseConfig == nil {
89- return nil , fmt .Errorf ("BaseConfig is required but not configured" )
90- }
114+ func (f * defaultRevisionEngineFactory ) createScopedClient (serviceAccountKey string ) (client.Client , error ) {
115+ // Parse the key back to namespace/name
116+ parts := strings .SplitN (serviceAccountKey , "/" , 2 )
117+ namespace , serviceAccountName := parts [0 ], parts [1 ]
91118
92119 saConfig := rest .AnonymousClientConfig (f .BaseConfig )
93120 saConfig .Wrap (func (rt http.RoundTripper ) http.RoundTripper {
@@ -105,13 +132,13 @@ func (f *DefaultRevisionEngineFactory) createScopedClient(namespace, serviceAcco
105132 Scheme : f .Scheme ,
106133 })
107134 if err != nil {
108- return nil , fmt .Errorf ("failed to create client for ServiceAccount %s/%s : %w" , namespace , serviceAccountName , err )
135+ return nil , fmt .Errorf ("failed to create client for ServiceAccount %s: %w" , serviceAccountKey , err )
109136 }
110137
111138 return scopedClient , nil
112139}
113140
114- // NewDefaultRevisionEngineFactory creates a new DefaultRevisionEngineFactory .
141+ // NewDefaultRevisionEngineFactory creates a new defaultRevisionEngineFactory .
115142func NewDefaultRevisionEngineFactory (
116143 scheme * runtime.Scheme ,
117144 trackingCache managedcache.TrackingCache ,
@@ -120,14 +147,20 @@ func NewDefaultRevisionEngineFactory(
120147 fieldOwnerPrefix string ,
121148 baseConfig * rest.Config ,
122149 tokenGetter * authentication.TokenGetter ,
123- ) * DefaultRevisionEngineFactory {
124- return & DefaultRevisionEngineFactory {
150+ ) (RevisionEngineFactory , error ) {
151+ if baseConfig == nil {
152+ return nil , fmt .Errorf ("baseConfig is required but not provided" )
153+ }
154+ if tokenGetter == nil {
155+ return nil , fmt .Errorf ("tokenGetter is required but not provided" )
156+ }
157+ return & defaultRevisionEngineFactory {
125158 Scheme : scheme ,
126159 TrackingCache : trackingCache ,
127160 DiscoveryClient : discoveryClient ,
128161 RESTMapper : restMapper ,
129162 FieldOwnerPrefix : fieldOwnerPrefix ,
130163 BaseConfig : baseConfig ,
131164 TokenGetter : tokenGetter ,
132- }
165+ }, nil
133166}
0 commit comments