diff --git a/cmd/operator.go b/cmd/operator.go index 3c0fb8e2..edc1a4d5 100644 --- a/cmd/operator.go +++ b/cmd/operator.go @@ -163,11 +163,6 @@ var operatorCmd = &cobra.Command{ return err } - if err = controller.NewStoreReconciler(ctx, log, fga, mgr, &operatorCfg). - SetupWithManager(mgr, defaultCfg); err != nil { - log.Error().Err(err).Str("controller", "store").Msg("unable to create controller") - return err - } if err = controller. NewAuthorizationModelReconciler(log, fga, mgr). SetupWithManager(mgr, defaultCfg); err != nil { diff --git a/cmd/system.go b/cmd/system.go index 36bf871e..33258dcf 100644 --- a/cmd/system.go +++ b/cmd/system.go @@ -7,6 +7,7 @@ import ( openfgav1 "github.com/openfga/api/proto/openfga/v1" platformeshcontext "github.com/platform-mesh/golang-commons/context" iclient "github.com/platform-mesh/security-operator/internal/client" + "github.com/platform-mesh/security-operator/internal/config" "github.com/platform-mesh/security-operator/internal/controller" "github.com/platform-mesh/security-operator/internal/fga" "github.com/spf13/cobra" @@ -16,6 +17,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + multiprovider "sigs.k8s.io/multicluster-runtime/providers/multi" "k8s.io/client-go/rest" @@ -63,15 +65,30 @@ var systemCmd = &cobra.Command{ opts.LeaderElectionConfig = inClusterCfg } - provider, err := apiexport.New(restCfg, systemCfg.APIExportEndpointSlices.SystemPlatformMeshIO, apiexport.Options{ + systemProvider, err := apiexport.New(restCfg, systemCfg.APIExportEndpointSlices.SystemPlatformMeshIO, apiexport.Options{ Scheme: scheme, }) if err != nil { - setupLog.Error(err, "unable to create apiexport provider") + setupLog.Error(err, "unable to create system apiexport provider") return err } - mgr, err := mcmanager.New(restCfg, provider, opts) + coreProvider, err := apiexport.New(restCfg, systemCfg.APIExportEndpointSlices.CorePlatformMeshIO, apiexport.Options{ + Scheme: scheme, + }) + if err != nil { + setupLog.Error(err, "unable to create core apiexport provider") + return err + } + multiProv := multiprovider.New(multiprovider.Options{}) + if err := multiProv.AddProvider(config.SystemProviderName, systemProvider); err != nil { + return err + } + if err := multiProv.AddProvider(config.CoreProviderName, coreProvider); err != nil { + return err + } + + mgr, err := mcmanager.New(restCfg, multiProv, opts) if err != nil { setupLog.Error(err, "unable to create manager") return err @@ -113,6 +130,12 @@ var systemCmd = &cobra.Command{ return err } + if err = controller.NewStoreReconciler(ctx, log, fgaClient, mgr, &operatorCfg). + SetupWithManager(mgr, defaultCfg); err != nil { + log.Error().Err(err).Str("controller", "store").Msg("unable to create controller") + return err + } + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { log.Error().Err(err).Msg("unable to set up health check") return err diff --git a/internal/config/config.go b/internal/config/config.go index 2d02197b..a1e5cb02 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,6 +7,12 @@ import ( "github.com/spf13/pflag" ) +const ( + CoreProviderName = "core" + SystemProviderName = "system" + ProviderSeparator = "#" +) + type KeycloakConfig struct { BaseURL string ClientID string diff --git a/internal/controller/apiexportpolicy_controller.go b/internal/controller/apiexportpolicy_controller.go index 9f224910..f87df634 100644 --- a/internal/controller/apiexportpolicy_controller.go +++ b/internal/controller/apiexportpolicy_controller.go @@ -68,7 +68,11 @@ func (r *APIExportPolicyReconciler) SetupWithManager(mgr mcmanager.Manager, cfg return mcbuilder.ControllerManagedBy(mgr). Named("apiexportpolicy"). - For(&corev1alpha1.APIExportPolicy{}). + For(&corev1alpha1.APIExportPolicy{}, + mcbuilder.WithClusterFilter(func(clusterName string, _ cluster.Cluster) bool { + return strings.HasPrefix(clusterName, config.SystemProviderName) + }), + ). WithOptions(opts). WithEventFilter(predicate.And(predicates...)). Watches( @@ -112,14 +116,16 @@ func (r *APIExportPolicyReconciler) enqueueAllAPIExportPolicies(ctx context.Cont trimmedExpr := strings.TrimPrefix(expr, ":") if trimmedExpr == "root:orgs:*" { - clusterName := logicalcluster.From(&policy) + // apiExportPolicies are engaged by system provider + clusterName := multiProviderName(config.SystemProviderName, logicalcluster.From(&policy).String()) + requests = append(requests, mcreconcile.Request{ Request: reconcile.Request{ NamespacedName: types.NamespacedName{ Name: policy.Name, }, }, - ClusterName: clusterName.String(), + ClusterName: clusterName, }) break } diff --git a/internal/controller/authorization_model_controller.go b/internal/controller/authorization_model_controller.go index 84a7233c..51c0ee70 100644 --- a/internal/controller/authorization_model_controller.go +++ b/internal/controller/authorization_model_controller.go @@ -8,6 +8,7 @@ import ( "github.com/platform-mesh/golang-commons/controller/filter" "github.com/platform-mesh/golang-commons/logger" corev1alpha1 "github.com/platform-mesh/security-operator/api/v1alpha1" + iclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/internal/subroutine" "github.com/platform-mesh/subroutines/lifecycle" ctrl "sigs.k8s.io/controller-runtime" @@ -25,9 +26,10 @@ type AuthorizationModelReconciler struct { } func NewAuthorizationModelReconciler(log *logger.Logger, fga openfgav1.OpenFGAServiceClient, mcMgr mcmanager.Manager) *AuthorizationModelReconciler { + kcpClientHelper := iclient.NewKcpHelper(mcMgr.GetLocalManager().GetConfig(), mcMgr.GetLocalManager().GetScheme()) lc := lifecycle.New(mcMgr, "AuthorizationModelReconciler", func() client.Object { return &corev1alpha1.AuthorizationModel{} - }, subroutine.NewTupleSubroutine(fga, mcMgr)) + }, subroutine.NewTupleSubroutine(fga, mcMgr, kcpClientHelper)) return &AuthorizationModelReconciler{ log: log, diff --git a/internal/controller/idp_controller.go b/internal/controller/idp_controller.go index 65de344c..7952f68f 100644 --- a/internal/controller/idp_controller.go +++ b/internal/controller/idp_controller.go @@ -3,6 +3,7 @@ package controller import ( "context" "fmt" + "strings" platformeshconfig "github.com/platform-mesh/golang-commons/config" "github.com/platform-mesh/golang-commons/controller/filter" @@ -15,6 +16,7 @@ import ( "github.com/platform-mesh/subroutines/lifecycle" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/cluster" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/predicate" mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder" @@ -65,7 +67,9 @@ func (r *IdentityProviderConfigurationReconciler) SetupWithManager(mgr mcmanager predicates := append([]predicate.Predicate{filter.DebugResourcesBehaviourPredicate(cfg.DebugLabelValue)}, evp...) return mcbuilder.ControllerManagedBy(mgr). Named("identityprovider"). - For(&corev1alpha1.IdentityProviderConfiguration{}). + For(&corev1alpha1.IdentityProviderConfiguration{}, mcbuilder.WithClusterFilter(func(clusterName string, _ cluster.Cluster) bool { + return strings.HasPrefix(clusterName, config.SystemProviderName) + })). WithOptions(opts). WithEventFilter(predicate.And(predicates...)). Complete(r) diff --git a/internal/controller/store_controller.go b/internal/controller/store_controller.go index 2f645eeb..62e7c769 100644 --- a/internal/controller/store_controller.go +++ b/internal/controller/store_controller.go @@ -2,6 +2,7 @@ package controller import ( "context" + "strings" openfgav1 "github.com/openfga/api/proto/openfga/v1" platformeshconfig "github.com/platform-mesh/golang-commons/config" @@ -42,6 +43,7 @@ func NewStoreReconciler(ctx context.Context, log *logger.Logger, fga openfgav1.O if err != nil { log.Fatal().Err(err).Msg("unable to create new client") } + kcpClientHelper := iclient.NewKcpHelper(mcMgr.GetLocalManager().GetConfig(), mcMgr.GetLocalManager().GetScheme()) lc := lifecycle.New(mcMgr, "StoreReconciler", func() client.Object { return &corev1alpha1.Store{} @@ -50,7 +52,7 @@ func NewStoreReconciler(ctx context.Context, log *logger.Logger, fga openfgav1.O subroutine.NewAuthorizationModelSubroutine(fga, mcMgr, allClient, func(cfg *rest.Config) discovery.DiscoveryInterface { return discovery.NewDiscoveryClientForConfigOrDie(cfg) }, log), - subroutine.NewTupleSubroutine(fga, mcMgr), + subroutine.NewTupleSubroutine(fga, mcMgr, kcpClientHelper), ).WithConditions(conditions.NewManager()) return &StoreReconciler{ @@ -69,7 +71,11 @@ func (r *StoreReconciler) SetupWithManager(mgr mcmanager.Manager, cfg *platforme predicates := append([]predicate.Predicate{filter.DebugResourcesBehaviourPredicate(cfg.DebugLabelValue)}, evp...) b := mcbuilder.ControllerManagedBy(mgr). Named("store"). - For(&corev1alpha1.Store{}). + For(&corev1alpha1.Store{}, + mcbuilder.WithClusterFilter(func(clusterName string, _ cluster.Cluster) bool { + return strings.HasPrefix(clusterName, config.SystemProviderName) + }), + ). WithOptions(controller.TypedOptions[mcreconcile.Request]{MaxConcurrentReconciles: cfg.MaxConcurrentReconciles}). WithEventFilter(predicate.And(predicates...)) @@ -82,6 +88,9 @@ func (r *StoreReconciler) SetupWithManager(mgr mcmanager.Manager, cfg *platforme if !ok { return nil } + // stores are engaged by system provider, to trigger a reconciliation with multi provider + // it's required to use provider's prefix for request + storeClusterName := multiProviderName(config.SystemProviderName, model.Spec.StoreRef.Cluster) return []mcreconcile.Request{ { @@ -90,11 +99,20 @@ func (r *StoreReconciler) SetupWithManager(mgr mcmanager.Manager, cfg *platforme Name: model.Spec.StoreRef.Name, }, }, - ClusterName: model.Spec.StoreRef.Cluster, + ClusterName: storeClusterName, }, } }) }, mcbuilder.WithPredicates(predicate.GenerationChangedPredicate{}), + mcbuilder.WithClusterFilter(func(clusterName string, _ cluster.Cluster) bool { + return strings.HasPrefix(clusterName, config.CoreProviderName) + }), ).Complete(r) } + +// multiProviderName returns a cluster name with provider prefix and separator for multi provider. +// The multi.Provider prefixes cluster names as "providerName#clusterName" +func multiProviderName(providerName, clusterName string) string { + return providerName + config.ProviderSeparator + clusterName +} diff --git a/internal/subroutine/authorization_model.go b/internal/subroutine/authorization_model.go index ca24aadf..275fa2b5 100644 --- a/internal/subroutine/authorization_model.go +++ b/internal/subroutine/authorization_model.go @@ -95,12 +95,6 @@ var _ subroutines.Processor = &authorizationModelSubroutine{} func (a *authorizationModelSubroutine) GetName() string { return "AuthorizationModel" } func getRelatedAuthorizationModels(ctx context.Context, k8s client.Client, store *securityv1alpha1.Store) (securityv1alpha1.AuthorizationModelList, error) { - - storeClusterKey, ok := mccontext.ClusterFrom(ctx) - if !ok { - return securityv1alpha1.AuthorizationModelList{}, fmt.Errorf("unable to get cluster key from context") - } - allCtx := mccontext.WithCluster(ctx, "") allAuthorizationModels := securityv1alpha1.AuthorizationModelList{} @@ -110,7 +104,7 @@ func getRelatedAuthorizationModels(ctx context.Context, k8s client.Client, store var extendingModules securityv1alpha1.AuthorizationModelList for _, model := range allAuthorizationModels.Items { - if model.Spec.StoreRef.Name != store.Name || model.Spec.StoreRef.Cluster != storeClusterKey { + if model.Spec.StoreRef.Name != store.Name { continue } diff --git a/internal/subroutine/tuples.go b/internal/subroutine/tuples.go index a92a8826..daa0ed8a 100644 --- a/internal/subroutine/tuples.go +++ b/internal/subroutine/tuples.go @@ -8,17 +8,21 @@ import ( openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/platform-mesh/golang-commons/logger" securityv1alpha1 "github.com/platform-mesh/security-operator/api/v1alpha1" + iclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/internal/fga" "github.com/platform-mesh/subroutines" "sigs.k8s.io/controller-runtime/pkg/client" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" "k8s.io/apimachinery/pkg/types" + + "github.com/kcp-dev/logicalcluster/v3" ) type tupleSubroutine struct { - fga openfgav1.OpenFGAServiceClient - mgr mcmanager.Manager + fga openfgav1.OpenFGAServiceClient + mgr mcmanager.Manager + kcpHelper iclient.KcpClientHelper } // Finalize implements subroutines.Finalizer. @@ -37,13 +41,13 @@ func (t *tupleSubroutine) Finalize(ctx context.Context, obj client.Object) (subr case *securityv1alpha1.AuthorizationModel: managedTuples = o.Status.ManagedTuples - storeCluster, err := t.mgr.GetCluster(ctx, o.Spec.StoreRef.Cluster) + cl, err := t.kcpHelper.NewClientForLogicalCluster(logicalcluster.Name(o.Spec.StoreRef.Cluster)) if err != nil { - return subroutines.OK(), fmt.Errorf("unable to get store cluster: %w", err) + return subroutines.OK(), fmt.Errorf("unable to create client to store cluster: %w", err) } var store securityv1alpha1.Store - err = storeCluster.GetClient().Get(ctx, types.NamespacedName{ + err = cl.Get(ctx, types.NamespacedName{ Name: o.Spec.StoreRef.Name, }, &store) if err != nil { @@ -97,13 +101,13 @@ func (t *tupleSubroutine) Process(ctx context.Context, obj client.Object) (subro specTuples = o.Spec.Tuples managedTuples = o.Status.ManagedTuples - storeCluster, err := t.mgr.GetCluster(ctx, o.Spec.StoreRef.Cluster) + cl, err := t.kcpHelper.NewClientForLogicalCluster(logicalcluster.Name(o.Spec.StoreRef.Cluster)) if err != nil { - return subroutines.OK(), fmt.Errorf("unable to get store cluster: %w", err) + return subroutines.OK(), fmt.Errorf("unable to create client to store cluster: %w", err) } var store securityv1alpha1.Store - err = storeCluster.GetClient().Get(ctx, types.NamespacedName{ + err = cl.Get(ctx, types.NamespacedName{ Name: o.Spec.StoreRef.Name, }, &store) if err != nil { @@ -141,10 +145,11 @@ func (t *tupleSubroutine) Process(ctx context.Context, obj client.Object) (subro return subroutines.OK(), nil } -func NewTupleSubroutine(fga openfgav1.OpenFGAServiceClient, mgr mcmanager.Manager) *tupleSubroutine { +func NewTupleSubroutine(fga openfgav1.OpenFGAServiceClient, mgr mcmanager.Manager, kcpHelper iclient.KcpClientHelper) *tupleSubroutine { return &tupleSubroutine{ - fga: fga, - mgr: mgr, + fga: fga, + mgr: mgr, + kcpHelper: kcpHelper, } } diff --git a/internal/subroutine/tuples_test.go b/internal/subroutine/tuples_test.go index 681767af..b28806ec 100644 --- a/internal/subroutine/tuples_test.go +++ b/internal/subroutine/tuples_test.go @@ -13,16 +13,20 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + + "github.com/kcp-dev/logicalcluster/v3" ) func TestTupleGetName(t *testing.T) { - subroutine := subroutine.NewTupleSubroutine(nil, nil) + subroutine := subroutine.NewTupleSubroutine(nil, nil, nil) assert.Equal(t, "TupleSubroutine", subroutine.GetName()) } func TestTupleFinalizers(t *testing.T) { - subroutine := subroutine.NewTupleSubroutine(nil, nil) + subroutine := subroutine.NewTupleSubroutine(nil, nil, nil) assert.Equal(t, []string{"core.platform-mesh.io/fga-tuples"}, subroutine.Finalizers(nil)) } @@ -164,7 +168,13 @@ func TestTupleProcessWithStore(t *testing.T) { test.mgrMocks(manager) } - subroutine := subroutine.NewTupleSubroutine(fga, manager) + // Mock GetLocalManager for Store tests + localMgr := mocks.NewCTRLManager(t) + manager.EXPECT().GetLocalManager().Return(localMgr).Maybe() + localMgr.EXPECT().GetConfig().Return(&rest.Config{}).Maybe() + localMgr.EXPECT().GetScheme().Return(runtime.NewScheme()).Maybe() + + subroutine := subroutine.NewTupleSubroutine(fga, manager, nil) _, err := subroutine.Process(context.Background(), test.store) if test.expectError { @@ -180,12 +190,11 @@ func TestTupleProcessWithStore(t *testing.T) { func TestTupleProcessWithAuthorizationModel(t *testing.T) { tests := []struct { - name string - store *securityv1alpha1.AuthorizationModel - fgaMocks func(*mocks.MockOpenFGAServiceClient) - k8sMocks func(*mocks.MockClient) - mgrMocks func(*mocks.MockManager) - expectError bool + name string + store *securityv1alpha1.AuthorizationModel + fgaMocks func(*mocks.MockOpenFGAServiceClient) + kcpHelperMocks func(*mocks.MockKcpHelper) + expectError bool }{ { name: "should process and add tuples to the authorization model", @@ -222,14 +231,9 @@ func TestTupleProcessWithAuthorizationModel(t *testing.T) { fgaMocks: func(fga *mocks.MockOpenFGAServiceClient) { fga.EXPECT().Write(mock.Anything, mock.Anything).Return(nil, nil) }, - k8sMocks: func(k8s *mocks.MockClient) { - // Not used for AuthorizationModel - }, - mgrMocks: func(mgr *mocks.MockManager) { - storeCluster := mocks.NewMockCluster(t) + kcpHelperMocks: func(kcpHelper *mocks.MockKcpHelper) { storeClient := mocks.NewMockClient(t) - mgr.EXPECT().GetCluster(mock.Anything, "store-cluster").Return(storeCluster, nil) - storeCluster.EXPECT().GetClient().Return(storeClient) + kcpHelper.EXPECT().NewClientForLogicalCluster(logicalcluster.Name("store-cluster")).Return(storeClient, nil) storeClient.EXPECT().Get(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, nn types.NamespacedName, o client.Object, opts ...client.GetOption) error { store := o.(*securityv1alpha1.Store) *store = securityv1alpha1.Store{ @@ -287,14 +291,9 @@ func TestTupleProcessWithAuthorizationModel(t *testing.T) { // Apply (batch write) + Delete (batch delete) fga.EXPECT().Write(mock.Anything, mock.Anything).Return(nil, nil).Twice() }, - k8sMocks: func(k8s *mocks.MockClient) { - // Not used for AuthorizationModel - }, - mgrMocks: func(mgr *mocks.MockManager) { - storeCluster := mocks.NewMockCluster(t) + kcpHelperMocks: func(kcpHelper *mocks.MockKcpHelper) { storeClient := mocks.NewMockClient(t) - mgr.EXPECT().GetCluster(mock.Anything, "store-cluster").Return(storeCluster, nil) - storeCluster.EXPECT().GetClient().Return(storeClient) + kcpHelper.EXPECT().NewClientForLogicalCluster(logicalcluster.Name("store-cluster")).Return(storeClient, nil) storeClient.EXPECT().Get(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, nn types.NamespacedName, o client.Object, opts ...client.GetOption) error { store := o.(*securityv1alpha1.Store) *store = securityv1alpha1.Store{ @@ -315,15 +314,12 @@ func TestTupleProcessWithAuthorizationModel(t *testing.T) { test.fgaMocks(fga) } - manager := mocks.NewMockManager(t) - if test.mgrMocks != nil { - test.mgrMocks(manager) - } - if test.k8sMocks != nil { - test.k8sMocks(mocks.NewMockClient(t)) + kcpHelper := mocks.NewMockKcpHelper(t) + if test.kcpHelperMocks != nil { + test.kcpHelperMocks(kcpHelper) } - subroutine := subroutine.NewTupleSubroutine(fga, manager) + subroutine := subroutine.NewTupleSubroutine(fga, nil, kcpHelper) ctx := context.Background() @@ -341,12 +337,11 @@ func TestTupleProcessWithAuthorizationModel(t *testing.T) { func TestTupleFinalizationWithAuthorizationModel(t *testing.T) { tests := []struct { - name string - store *securityv1alpha1.AuthorizationModel - fgaMocks func(*mocks.MockOpenFGAServiceClient) - k8sMocks func(*mocks.MockClient) - mgrMocks func(*mocks.MockManager) - expectError bool + name string + store *securityv1alpha1.AuthorizationModel + fgaMocks func(*mocks.MockOpenFGAServiceClient) + kcpHelperMocks func(*mocks.MockKcpHelper) + expectError bool }{ { name: "should finalize the authorization model", @@ -376,14 +371,9 @@ func TestTupleFinalizationWithAuthorizationModel(t *testing.T) { // delete call fga.EXPECT().Write(mock.Anything, mock.Anything).Return(nil, nil) }, - k8sMocks: func(k8s *mocks.MockClient) { - // Not used for AuthorizationModel - }, - mgrMocks: func(mgr *mocks.MockManager) { - storeCluster := mocks.NewMockCluster(t) + kcpHelperMocks: func(kcpHelper *mocks.MockKcpHelper) { storeClient := mocks.NewMockClient(t) - mgr.EXPECT().GetCluster(mock.Anything, "store-cluster").Return(storeCluster, nil) - storeCluster.EXPECT().GetClient().Return(storeClient) + kcpHelper.EXPECT().NewClientForLogicalCluster(logicalcluster.Name("store-cluster")).Return(storeClient, nil) storeClient.EXPECT().Get(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, nn types.NamespacedName, o client.Object, opts ...client.GetOption) error { store := o.(*securityv1alpha1.Store) *store = securityv1alpha1.Store{ @@ -404,15 +394,12 @@ func TestTupleFinalizationWithAuthorizationModel(t *testing.T) { test.fgaMocks(fga) } - manager := mocks.NewMockManager(t) - if test.mgrMocks != nil { - test.mgrMocks(manager) - } - if test.k8sMocks != nil { - test.k8sMocks(mocks.NewMockClient(t)) + kcpHelper := mocks.NewMockKcpHelper(t) + if test.kcpHelperMocks != nil { + test.kcpHelperMocks(kcpHelper) } - subroutine := subroutine.NewTupleSubroutine(fga, manager) + subroutine := subroutine.NewTupleSubroutine(fga, nil, kcpHelper) ctx := context.Background() @@ -487,7 +474,13 @@ func TestTupleFinalizationWithStore(t *testing.T) { test.mgrMocks(manager) } - subroutine := subroutine.NewTupleSubroutine(fga, manager) + // Mock GetLocalManager for Store tests + localMgr := mocks.NewCTRLManager(t) + manager.EXPECT().GetLocalManager().Return(localMgr).Maybe() + localMgr.EXPECT().GetConfig().Return(&rest.Config{}).Maybe() + localMgr.EXPECT().GetScheme().Return(runtime.NewScheme()).Maybe() + + subroutine := subroutine.NewTupleSubroutine(fga, manager, nil) _, err := subroutine.Finalize(context.Background(), test.store) if test.expectError {