From 14bd268bed17ee9f1c7a8d45083d3e770ab6aef0 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 28 Jan 2026 07:42:33 +0000 Subject: [PATCH 01/61] feat: add separate reconciler to initialize account workspaces of type account --- cmd/initializer.go | 26 ++++++- .../accountlogicalcluster_controller.go | 49 +++++++++++++ internal/predicates/accounttype.go | 20 ++++++ internal/subroutine/account_tuples.go | 69 +++++++++++++++++++ 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 internal/controller/accountlogicalcluster_controller.go create mode 100644 internal/predicates/accounttype.go create mode 100644 internal/subroutine/account_tuples.go diff --git a/cmd/initializer.go b/cmd/initializer.go index 6fc43aa3..9167e9ac 100644 --- a/cmd/initializer.go +++ b/cmd/initializer.go @@ -6,12 +6,18 @@ import ( helmv2 "github.com/fluxcd/helm-controller/api/v2" sourcev1 "github.com/fluxcd/source-controller/api/v1" + mcclient "github.com/kcp-dev/multicluster-provider/client" + openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/platform-mesh/security-operator/internal/controller" + "github.com/platform-mesh/security-operator/internal/predicates" "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/predicate" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" "k8s.io/apimachinery/pkg/runtime" @@ -76,6 +82,13 @@ var initializerCmd = &cobra.Command{ utilruntime.Must(sourcev1.AddToScheme(runtimeScheme)) utilruntime.Must(helmv2.AddToScheme(runtimeScheme)) + conn, err := grpc.NewClient(operatorCfg.FGA.Target, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Error().Err(err).Msg("unable to create grpc client") + return err + } + fga := openfgav1.NewOpenFGAServiceClient(conn) + orgClient, err := logicalClusterClientFromKey(mgr.GetLocalManager().GetConfig(), log)(logicalcluster.Name("root:orgs")) if err != nil { setupLog.Error(err, "Failed to create org client") @@ -95,11 +108,22 @@ var initializerCmd = &cobra.Command{ } if err := controller.NewLogicalClusterReconciler(log, orgClient, initializerCfg, runtimeClient, mgr). - SetupWithManager(mgr, defaultCfg); err != nil { + SetupWithManager(mgr, defaultCfg, predicates.IsAccountTypeOrg()); err != nil { setupLog.Error(err, "unable to create controller", "controller", "LogicalCluster") os.Exit(1) } + mcc, err := mcclient.New(k8sCfg, client.Options{Scheme: scheme}) + if err != nil { + log.Error().Err(err).Msg("Failed to create multicluster client") + os.Exit(1) + } + if err := controller.NewAccountLogicalClusterReconciler(log, fga, initializerCfg, mcc, mgr). + SetupWithManager(mgr, defaultCfg, predicate.Not(predicates.IsAccountTypeOrg())); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "AccountLogicalCluster") + os.Exit(1) + } + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) diff --git a/internal/controller/accountlogicalcluster_controller.go b/internal/controller/accountlogicalcluster_controller.go new file mode 100644 index 00000000..82178ddc --- /dev/null +++ b/internal/controller/accountlogicalcluster_controller.go @@ -0,0 +1,49 @@ +package controller + +import ( + "context" + + mcclient "github.com/kcp-dev/multicluster-provider/client" + openfgav1 "github.com/openfga/api/proto/openfga/v1" + platformeshconfig "github.com/platform-mesh/golang-commons/config" + "github.com/platform-mesh/golang-commons/controller/lifecycle/builder" + "github.com/platform-mesh/golang-commons/controller/lifecycle/multicluster" + lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" + "github.com/platform-mesh/golang-commons/logger" + "github.com/platform-mesh/security-operator/internal/config" + "github.com/platform-mesh/security-operator/internal/subroutine" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/predicate" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" +) + +type AccountLogicalClusterReconciler struct { + log *logger.Logger + + mclifecycle *multicluster.LifecycleManager +} + +func NewAccountLogicalClusterReconciler(log *logger.Logger, fga openfgav1.OpenFGAServiceClient, cfg config.Config, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountLogicalClusterReconciler { + return &AccountLogicalClusterReconciler{ + log: log, + mclifecycle: builder.NewBuilder("security", "AccountLogicalClusterReconciler", []lifecyclesubroutine.Subroutine{ + subroutine.NewAccountTuplesSubroutine(fga, mcc, mgr), + }, log). + WithReadOnly(). + WithStaticThenExponentialRateLimiter(). + BuildMultiCluster(mgr), + } +} + +func (r *AccountLogicalClusterReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) { + ctxWithCluster := mccontext.WithCluster(ctx, req.ClusterName) + return r.mclifecycle.Reconcile(ctxWithCluster, req, &kcpcorev1alpha1.LogicalCluster{}) +} + +func (r *AccountLogicalClusterReconciler) SetupWithManager(mgr mcmanager.Manager, cfg *platformeshconfig.CommonServiceConfig, evp ...predicate.Predicate) error { + return r.mclifecycle.SetupWithManager(mgr, cfg.MaxConcurrentReconciles, "AccountLogicalCluster", &kcpcorev1alpha1.LogicalCluster{}, cfg.DebugLabelValue, r, r.log, evp...) +} diff --git a/internal/predicates/accounttype.go b/internal/predicates/accounttype.go new file mode 100644 index 00000000..082e5f0c --- /dev/null +++ b/internal/predicates/accounttype.go @@ -0,0 +1,20 @@ +package predicates + +import ( + "strings" + + kcpcore "github.com/kcp-dev/sdk/apis/core" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// IsAccountTypeOrg returns a predicate that filters for LogicalClusters +// belonging to an Account of type "org". +// todo(simontesar): more stable implementation not relying on static orgs path +func IsAccountTypeOrg() predicate.Predicate { + return predicate.NewPredicateFuncs(func(object client.Object) bool { + a := object.GetAnnotations() + lc := a[kcpcore.LogicalClusterPathAnnotationKey] + return strings.Contains(lc, "/orgs/") + }) +} diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go new file mode 100644 index 00000000..74cdf855 --- /dev/null +++ b/internal/subroutine/account_tuples.go @@ -0,0 +1,69 @@ +package subroutine + +import ( + "context" + "fmt" + + mcclient "github.com/kcp-dev/multicluster-provider/client" + kcpcore "github.com/kcp-dev/sdk/apis/core" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" + lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" + "github.com/platform-mesh/golang-commons/errors" + "github.com/platform-mesh/golang-commons/logger" + ctrl "sigs.k8s.io/controller-runtime" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" +) + +type AccountTuplesSubroutine struct { + fga openfgav1.OpenFGAServiceClient + mgr mcmanager.Manager + mcc mcclient.ClusterClient +} + +// Finalize implements lifecycle.Subroutine. +func (s *AccountTuplesSubroutine) Finalize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + log := logger.LoadLoggerFromContext(ctx) + + lc := instance.(*kcpcorev1alpha1.LogicalCluster) + p := lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] + if p == "" { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster is not set"), true, true) + } + log.Info().Msgf("Finalizing logical cluster of path %s", p) + + return ctrl.Result{}, nil +} + +// Finalizers implements lifecycle.Subroutine. +func (s *AccountTuplesSubroutine) Finalizers(_ runtimeobject.RuntimeObject) []string { + return []string{"core.platform-mesh.io/account-fga-tuples"} +} + +// GetName implements lifecycle.Subroutine. +func (s *AccountTuplesSubroutine) GetName() string { return "AccountTuplesSubroutine" } + +// Process implements lifecycle.Subroutine. +func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + log := logger.LoadLoggerFromContext(ctx) + + lc := instance.(*kcpcorev1alpha1.LogicalCluster) + p := lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] + if p == "" { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster is not set"), true, true) + } + log.Info().Msgf("Processing logical cluster of path %s", p) + + return ctrl.Result{}, nil +} + +func NewAccountTuplesSubroutine(fga openfgav1.OpenFGAServiceClient, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountTuplesSubroutine { + return &AccountTuplesSubroutine{ + fga: fga, + mgr: mgr, + mcc: mcc, + } +} + +var _ lifecyclesubroutine.Subroutine = &AccountTuplesSubroutine{} From 03cac88ca9e93e1766434886139cfec61307afbf Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 28 Jan 2026 12:26:02 +0000 Subject: [PATCH 02/61] fix: correct accounttype predicate --- internal/predicates/accounttype.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/predicates/accounttype.go b/internal/predicates/accounttype.go index 082e5f0c..1c99c1a7 100644 --- a/internal/predicates/accounttype.go +++ b/internal/predicates/accounttype.go @@ -15,6 +15,6 @@ func IsAccountTypeOrg() predicate.Predicate { return predicate.NewPredicateFuncs(func(object client.Object) bool { a := object.GetAnnotations() lc := a[kcpcore.LogicalClusterPathAnnotationKey] - return strings.Contains(lc, "/orgs/") + return strings.Contains(lc, ":orgs:") }) } From 0e1bd75ca14a6bb710d5d454aaedfe42f57fa817 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 28 Jan 2026 12:26:47 +0000 Subject: [PATCH 03/61] feat: use openfga SDK --- cmd/initializer.go | 12 ++++++------ go.mod | 1 + go.sum | 2 ++ .../controller/accountlogicalcluster_controller.go | 4 ++-- internal/subroutine/account_tuples.go | 11 +++++++---- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/cmd/initializer.go b/cmd/initializer.go index 9167e9ac..7e1752b8 100644 --- a/cmd/initializer.go +++ b/cmd/initializer.go @@ -7,12 +7,10 @@ import ( helmv2 "github.com/fluxcd/helm-controller/api/v2" sourcev1 "github.com/fluxcd/source-controller/api/v1" mcclient "github.com/kcp-dev/multicluster-provider/client" - openfgav1 "github.com/openfga/api/proto/openfga/v1" + openfga "github.com/openfga/go-sdk" "github.com/platform-mesh/security-operator/internal/controller" "github.com/platform-mesh/security-operator/internal/predicates" "github.com/spf13/cobra" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" @@ -82,12 +80,14 @@ var initializerCmd = &cobra.Command{ utilruntime.Must(sourcev1.AddToScheme(runtimeScheme)) utilruntime.Must(helmv2.AddToScheme(runtimeScheme)) - conn, err := grpc.NewClient(operatorCfg.FGA.Target, grpc.WithTransportCredentials(insecure.NewCredentials())) + fgaConfiguration, err := openfga.NewConfiguration(openfga.Configuration{ + ApiUrl: operatorCfg.FGA.Target, + }) if err != nil { - log.Error().Err(err).Msg("unable to create grpc client") + log.Error().Err(err).Msg("unable to create OpenFGA client") return err } - fga := openfgav1.NewOpenFGAServiceClient(conn) + fga := openfga.NewAPIClient(fgaConfiguration) orgClient, err := logicalClusterClientFromKey(mgr.GetLocalManager().GetConfig(), log)(logicalcluster.Name("root:orgs")) if err != nil { diff --git a/go.mod b/go.mod index 415f9a55..24f8d8c8 100644 --- a/go.mod +++ b/go.mod @@ -74,6 +74,7 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/gomega v1.38.2 // indirect + github.com/openfga/go-sdk v0.7.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect diff --git a/go.sum b/go.sum index 35c020e3..131eeb9e 100644 --- a/go.sum +++ b/go.sum @@ -151,6 +151,8 @@ github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/openfga/api/proto v0.0.0-20260122181957-618e7e0a4878 h1:BAg/v38U3stiKx/po8k8F5Sgt4U8KP3+1jBt/aKQMsI= github.com/openfga/api/proto v0.0.0-20260122181957-618e7e0a4878/go.mod h1:XDX4qYNBUM2Rsa2AbKPh+oocZc2zgme+EF2fFC6amVU= +github.com/openfga/go-sdk v0.7.4 h1:WBZDjl5Aqy1pFsDCL9LGZ5teJsYh42giFWA7G4AHfkw= +github.com/openfga/go-sdk v0.7.4/go.mod h1:jGyDrPZauqrGM89iSqvjVwwF80fKCTOIERGZ+X3H4pI= github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c h1:xPbHNFG8QbPr/fpL7u0MPI0x74/BCLm7Sx02btL1m5Q= github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c/go.mod h1:BG26d1Fk4GSg0wMj60TRJ6Pe4ka2WQ33akhO+mzt3t0= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= diff --git a/internal/controller/accountlogicalcluster_controller.go b/internal/controller/accountlogicalcluster_controller.go index 82178ddc..824fb175 100644 --- a/internal/controller/accountlogicalcluster_controller.go +++ b/internal/controller/accountlogicalcluster_controller.go @@ -4,7 +4,7 @@ import ( "context" mcclient "github.com/kcp-dev/multicluster-provider/client" - openfgav1 "github.com/openfga/api/proto/openfga/v1" + openfga "github.com/openfga/go-sdk" platformeshconfig "github.com/platform-mesh/golang-commons/config" "github.com/platform-mesh/golang-commons/controller/lifecycle/builder" "github.com/platform-mesh/golang-commons/controller/lifecycle/multicluster" @@ -27,7 +27,7 @@ type AccountLogicalClusterReconciler struct { mclifecycle *multicluster.LifecycleManager } -func NewAccountLogicalClusterReconciler(log *logger.Logger, fga openfgav1.OpenFGAServiceClient, cfg config.Config, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountLogicalClusterReconciler { +func NewAccountLogicalClusterReconciler(log *logger.Logger, fga *openfga.APIClient, cfg config.Config, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountLogicalClusterReconciler { return &AccountLogicalClusterReconciler{ log: log, mclifecycle: builder.NewBuilder("security", "AccountLogicalClusterReconciler", []lifecyclesubroutine.Subroutine{ diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index 74cdf855..b3017cb6 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -7,17 +7,19 @@ import ( mcclient "github.com/kcp-dev/multicluster-provider/client" kcpcore "github.com/kcp-dev/sdk/apis/core" kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" - openfgav1 "github.com/openfga/api/proto/openfga/v1" + openfga "github.com/openfga/go-sdk" + "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/errors" "github.com/platform-mesh/golang-commons/logger" ctrl "sigs.k8s.io/controller-runtime" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" ) type AccountTuplesSubroutine struct { - fga openfgav1.OpenFGAServiceClient + fga *openfga.APIClient mgr mcmanager.Manager mcc mcclient.ClusterClient } @@ -53,12 +55,13 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo if p == "" { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster is not set"), true, true) } - log.Info().Msgf("Processing logical cluster of path %s", p) + cluster, _ := mccontext.ClusterFrom(ctx) + log.Info().Msgf("Processing logical cluster of path %s with %s in context", p, cluster) return ctrl.Result{}, nil } -func NewAccountTuplesSubroutine(fga openfgav1.OpenFGAServiceClient, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountTuplesSubroutine { +func NewAccountTuplesSubroutine(fga *openfga.APIClient, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountTuplesSubroutine { return &AccountTuplesSubroutine{ fga: fga, mgr: mgr, From f5b771c0506548c7e871ae84a2d92440d6a97897 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 3 Feb 2026 08:27:56 +0000 Subject: [PATCH 04/61] fix: org predicate checks owner not path --- internal/predicates/accounttype.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/predicates/accounttype.go b/internal/predicates/accounttype.go index 1c99c1a7..9789a263 100644 --- a/internal/predicates/accounttype.go +++ b/internal/predicates/accounttype.go @@ -1,9 +1,8 @@ package predicates import ( - "strings" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" - kcpcore "github.com/kcp-dev/sdk/apis/core" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/predicate" ) @@ -13,8 +12,8 @@ import ( // todo(simontesar): more stable implementation not relying on static orgs path func IsAccountTypeOrg() predicate.Predicate { return predicate.NewPredicateFuncs(func(object client.Object) bool { - a := object.GetAnnotations() - lc := a[kcpcore.LogicalClusterPathAnnotationKey] - return strings.Contains(lc, ":orgs:") + lc := object.(*kcpcorev1alpha1.LogicalCluster) + + return lc.Spec.Owner.Name == "orgs" }) } From 1d23cd70568322bd9bd2e93a7a5886bbd3f5162d Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 3 Feb 2026 08:28:41 +0000 Subject: [PATCH 05/61] fix: have account initializer also use remove-initializer subroutine --- internal/controller/accountlogicalcluster_controller.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/controller/accountlogicalcluster_controller.go b/internal/controller/accountlogicalcluster_controller.go index 824fb175..246b2043 100644 --- a/internal/controller/accountlogicalcluster_controller.go +++ b/internal/controller/accountlogicalcluster_controller.go @@ -32,6 +32,7 @@ func NewAccountLogicalClusterReconciler(log *logger.Logger, fga *openfga.APIClie log: log, mclifecycle: builder.NewBuilder("security", "AccountLogicalClusterReconciler", []lifecyclesubroutine.Subroutine{ subroutine.NewAccountTuplesSubroutine(fga, mcc, mgr), + subroutine.NewRemoveInitializer(mgr, cfg), }, log). WithReadOnly(). WithStaticThenExponentialRateLimiter(). From f00315a15040bcd25ae89f7f58cdae04ee6fe88f Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 3 Feb 2026 09:37:12 +0000 Subject: [PATCH 06/61] refactor: consistent file naming --- ...{initializer_controller.go => orglogicalcluster_controller.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/controller/{initializer_controller.go => orglogicalcluster_controller.go} (100%) diff --git a/internal/controller/initializer_controller.go b/internal/controller/orglogicalcluster_controller.go similarity index 100% rename from internal/controller/initializer_controller.go rename to internal/controller/orglogicalcluster_controller.go From 464faf87ca3e2ffc626628fe4aa59b3ab80d2792 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 4 Feb 2026 08:06:09 +0000 Subject: [PATCH 07/61] feat: implement account initializer using Store resource On-behalf-of: @SAP --- cmd/initializer.go | 20 +-- go.mod | 1 - internal/config/config.go | 5 +- .../accountlogicalcluster_controller.go | 5 +- internal/subroutine/account_tuples.go | 158 +++++++++++++++--- 5 files changed, 153 insertions(+), 36 deletions(-) diff --git a/cmd/initializer.go b/cmd/initializer.go index 7e1752b8..46b61284 100644 --- a/cmd/initializer.go +++ b/cmd/initializer.go @@ -7,7 +7,6 @@ import ( helmv2 "github.com/fluxcd/helm-controller/api/v2" sourcev1 "github.com/fluxcd/source-controller/api/v1" mcclient "github.com/kcp-dev/multicluster-provider/client" - openfga "github.com/openfga/go-sdk" "github.com/platform-mesh/security-operator/internal/controller" "github.com/platform-mesh/security-operator/internal/predicates" "github.com/spf13/cobra" @@ -80,15 +79,6 @@ var initializerCmd = &cobra.Command{ utilruntime.Must(sourcev1.AddToScheme(runtimeScheme)) utilruntime.Must(helmv2.AddToScheme(runtimeScheme)) - fgaConfiguration, err := openfga.NewConfiguration(openfga.Configuration{ - ApiUrl: operatorCfg.FGA.Target, - }) - if err != nil { - log.Error().Err(err).Msg("unable to create OpenFGA client") - return err - } - fga := openfga.NewAPIClient(fgaConfiguration) - orgClient, err := logicalClusterClientFromKey(mgr.GetLocalManager().GetConfig(), log)(logicalcluster.Name("root:orgs")) if err != nil { setupLog.Error(err, "Failed to create org client") @@ -113,12 +103,18 @@ var initializerCmd = &cobra.Command{ os.Exit(1) } - mcc, err := mcclient.New(k8sCfg, client.Options{Scheme: scheme}) + kcpCfg, err := getKubeconfigFromPath(operatorCfg.KCP.Kubeconfig) + if err != nil { + log.Error().Err(err).Msg("unable to get KCP kubeconfig") + return err + } + + mcc, err := mcclient.New(kcpCfg, client.Options{Scheme: scheme}) if err != nil { log.Error().Err(err).Msg("Failed to create multicluster client") os.Exit(1) } - if err := controller.NewAccountLogicalClusterReconciler(log, fga, initializerCfg, mcc, mgr). + if err := controller.NewAccountLogicalClusterReconciler(log, initializerCfg, mcc, mgr). SetupWithManager(mgr, defaultCfg, predicate.Not(predicates.IsAccountTypeOrg())); err != nil { setupLog.Error(err, "unable to create controller", "controller", "AccountLogicalCluster") os.Exit(1) diff --git a/go.mod b/go.mod index 480129d9..8195e088 100644 --- a/go.mod +++ b/go.mod @@ -74,7 +74,6 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/gomega v1.38.2 // indirect - github.com/openfga/go-sdk v0.7.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect diff --git a/internal/config/config.go b/internal/config/config.go index b2a1589d..31ca1078 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,7 +9,10 @@ type InviteConfig struct { // Config struct to hold the app config type Config struct { FGA struct { - Target string `mapstructure:"fga-target"` + Target string `mapstructure:"fga-target"` + ObjectType string `mapstructure:"subroutines-fga-object-type" default:"core_platform-mesh_io_account"` + ParentRelation string `mapstructure:"subroutines-fga-parent-relation" default:"parent"` + CreatorRelation string `mapstructure:"subroutines-fga-creator-relation" default:"owner"` } `mapstructure:",squash"` KCP struct { Kubeconfig string `mapstructure:"kcp-kubeconfig" default:"/api-kubeconfig/kubeconfig"` diff --git a/internal/controller/accountlogicalcluster_controller.go b/internal/controller/accountlogicalcluster_controller.go index 246b2043..f68812e6 100644 --- a/internal/controller/accountlogicalcluster_controller.go +++ b/internal/controller/accountlogicalcluster_controller.go @@ -4,7 +4,6 @@ import ( "context" mcclient "github.com/kcp-dev/multicluster-provider/client" - openfga "github.com/openfga/go-sdk" platformeshconfig "github.com/platform-mesh/golang-commons/config" "github.com/platform-mesh/golang-commons/controller/lifecycle/builder" "github.com/platform-mesh/golang-commons/controller/lifecycle/multicluster" @@ -27,11 +26,11 @@ type AccountLogicalClusterReconciler struct { mclifecycle *multicluster.LifecycleManager } -func NewAccountLogicalClusterReconciler(log *logger.Logger, fga *openfga.APIClient, cfg config.Config, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountLogicalClusterReconciler { +func NewAccountLogicalClusterReconciler(log *logger.Logger, cfg config.Config, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountLogicalClusterReconciler { return &AccountLogicalClusterReconciler{ log: log, mclifecycle: builder.NewBuilder("security", "AccountLogicalClusterReconciler", []lifecyclesubroutine.Subroutine{ - subroutine.NewAccountTuplesSubroutine(fga, mcc, mgr), + subroutine.NewAccountTuplesSubroutine(mcc, mgr, cfg.FGA.CreatorRelation, cfg.FGA.ParentRelation, cfg.FGA.ObjectType), subroutine.NewRemoveInitializer(mgr, cfg), }, log). WithReadOnly(). diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index b3017cb6..bb82edca 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -3,25 +3,123 @@ package subroutine import ( "context" "fmt" + "net/url" + "slices" + "strings" + "github.com/kcp-dev/logicalcluster/v3" mcclient "github.com/kcp-dev/multicluster-provider/client" kcpcore "github.com/kcp-dev/sdk/apis/core" kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" - openfga "github.com/openfga/go-sdk" + "github.com/rs/zerolog/log" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + accountsv1alpha1 "github.com/platform-mesh/account-operator/api/v1alpha1" "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/errors" "github.com/platform-mesh/golang-commons/logger" + "github.com/platform-mesh/security-operator/api/v1alpha1" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" ) type AccountTuplesSubroutine struct { - fga *openfga.APIClient mgr mcmanager.Manager mcc mcclient.ClusterClient + + objectType string + parentRelation string + creatorRelation string +} + +// Process implements lifecycle.Subroutine. +func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + log := logger.LoadLoggerFromContext(ctx) + + lc := instance.(*kcpcorev1alpha1.LogicalCluster) + p := lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] + if p == "" { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster is not set"), true, true) + } + lcID, _ := mccontext.ClusterFrom(ctx) + log = log.ChildLogger("ID", lcID).ChildLogger("path", p) + log.Info().Msgf("Processing logical cluster") + + lcClient, err := NewLCClient(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(lcID)) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting client: %w", err), true, true) + } + + var ai accountsv1alpha1.AccountInfo + if err := lcClient.Get(ctx, client.ObjectKey{ + Name: accountsv1alpha1.DefaultAccountInfoName, + }, &ai); err != nil && !kerrors.IsNotFound(err) { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting AccountInfo for LogicalCluster: %w", err), true, true) + } else if kerrors.IsNotFound(err) { + fmt.Println(err) + + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("AccountInfo not found yet, requeueing"), true, false) + } + + parentOrgClient, err := NewLCClient(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(ai.Spec.Organization.Path)) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting parent organisation client: %w", err), true, true) + } + + var acc accountsv1alpha1.Account + if err := parentOrgClient.Get(ctx, client.ObjectKey{ + Name: ai.Spec.Account.Name, + }, &acc); err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Account in parent organisation: %w", err), true, true) + } + + orgsClient, err := NewLCClient(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name("root:orgs")) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting orgs client: %w", err), true, true) + } + + var st v1alpha1.Store + if err := orgsClient.Get(ctx, client.ObjectKey{ + Name: ai.Spec.Organization.Name, + }, &st); err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting parent organisation's Store: %w", err), true, true) + } + + tuples := []v1alpha1.Tuple{ + v1alpha1.Tuple{ + User: fmt.Sprintf("%s:%s/%s", s.objectType, ai.Spec.ParentAccount.OriginClusterId, ai.Spec.ParentAccount.Name), + Relation: s.parentRelation, + Object: fmt.Sprintf("%s:%s/%s", s.objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), + }, + v1alpha1.Tuple{ + User: fmt.Sprintf("user:%s", formatUser(*acc.Spec.Creator)), + Relation: "assignee", + Object: fmt.Sprintf("role:%s/%s/%s/owner", s.objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), + }, + v1alpha1.Tuple{ + User: fmt.Sprintf("role:%s/%s/%s/owner#assignee", s.objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), + Relation: s.creatorRelation, + Object: fmt.Sprintf("%s:%s/%s", s.objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), + }, + } + + // Append the stores tuples with every tuple for the Account not yet managed + // via the Store resource + for _, t := range tuples { + if !slices.Contains(st.Spec.Tuples, t) { + st.Spec.Tuples = append(st.Spec.Tuples, t) + } + } + + if err := orgsClient.Update(ctx, &st); err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("updating Store with tuples: %w", err), true, true) + } + return ctrl.Result{}, nil } // Finalize implements lifecycle.Subroutine. @@ -46,27 +144,49 @@ func (s *AccountTuplesSubroutine) Finalizers(_ runtimeobject.RuntimeObject) []st // GetName implements lifecycle.Subroutine. func (s *AccountTuplesSubroutine) GetName() string { return "AccountTuplesSubroutine" } -// Process implements lifecycle.Subroutine. -func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { - log := logger.LoadLoggerFromContext(ctx) +func NewAccountTuplesSubroutine(mcc mcclient.ClusterClient, mgr mcmanager.Manager, creatorRelation, parentRelation, objectType string) *AccountTuplesSubroutine { + return &AccountTuplesSubroutine{ + mgr: mgr, + mcc: mcc, + creatorRelation: creatorRelation, + parentRelation: parentRelation, + objectType: objectType, + } +} - lc := instance.(*kcpcorev1alpha1.LogicalCluster) - p := lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] - if p == "" { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster is not set"), true, true) +var _ lifecyclesubroutine.Subroutine = &AccountTuplesSubroutine{} + +func NewLCClient(config *rest.Config, scheme *runtime.Scheme, clusterKey logicalcluster.Name) (client.Client, error) { + cfg := rest.CopyConfig(config) + + parsed, err := url.Parse(cfg.Host) + if err != nil { + log.Error().Err(err).Msg("unable to parse host") + panic(err) } - cluster, _ := mccontext.ClusterFrom(ctx) - log.Info().Msgf("Processing logical cluster of path %s with %s in context", p, cluster) - return ctrl.Result{}, nil + parsed.Path = fmt.Sprintf("/clusters/%s", clusterKey) + + cfg.Host = parsed.String() + + return client.New(cfg, client.Options{ + Scheme: scheme, + }) } -func NewAccountTuplesSubroutine(fga *openfga.APIClient, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountTuplesSubroutine { - return &AccountTuplesSubroutine{ - fga: fga, - mgr: mgr, - mcc: mcc, - } +// isServiceAccount determines wheter a user appears to be a Kubernetes +// ServiceAccount. +func isServiceAccount(user string) bool { + return strings.HasPrefix(user, "system:serviceaccount:") } -var _ lifecyclesubroutine.Subroutine = &AccountTuplesSubroutine{} +// formatUser formats a user to be stored in an FGA tuple, i.e. replaces colons +// with dots in case of a Kubernetes ServiceAccount. +// todo(simontesar): why was this implemented ot only be done in case of SAs? +func formatUser(user string) string { + if isServiceAccount(user) { + return strings.ReplaceAll(user, ":", ".") + } + + return user +} From 56ae52c7eba7cd4dfab5d3fe0b9b7b03e1a027c9 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 4 Feb 2026 10:02:01 +0000 Subject: [PATCH 08/61] feat: add note about not checking condition On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index bb82edca..f65e72c4 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -119,6 +119,17 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo if err := orgsClient.Update(ctx, &st); err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("updating Store with tuples: %w", err), true, true) } + + // todo(simontesar): checking and waiting for Readiness is currently futile + // our conditions don't include the observed generation + // + // if err := orgsClient.Get(ctx, client.ObjectKey{Name: st.Name}, &st); err != nil { + // return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Store after update: %w", err), true, true) + // } + // if conditions.IsPresentAndEqualForGeneration(st.Status.Conditions, lcconditions.ConditionReady, metav1.ConditionTrue, st.GetObjectMeta().GetGeneration()) { + // return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("store %s is not ready", st.Name), true, false) + // } + return ctrl.Result{}, nil } From 85a55342abbbb2c2bd7fad6a5dd7d7ec9fad72f8 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Thu, 5 Feb 2026 12:02:50 +0000 Subject: [PATCH 09/61] fix: rework org account type predicate On-behalf-of: @SAP --- cmd/initializer.go | 4 ++-- internal/predicates/accounttype.go | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/cmd/initializer.go b/cmd/initializer.go index 46b61284..3c866b84 100644 --- a/cmd/initializer.go +++ b/cmd/initializer.go @@ -98,7 +98,7 @@ var initializerCmd = &cobra.Command{ } if err := controller.NewLogicalClusterReconciler(log, orgClient, initializerCfg, runtimeClient, mgr). - SetupWithManager(mgr, defaultCfg, predicates.IsAccountTypeOrg()); err != nil { + SetupWithManager(mgr, defaultCfg, predicates.LogicalClusterIsAccountTypeOrg()); err != nil { setupLog.Error(err, "unable to create controller", "controller", "LogicalCluster") os.Exit(1) } @@ -115,7 +115,7 @@ var initializerCmd = &cobra.Command{ os.Exit(1) } if err := controller.NewAccountLogicalClusterReconciler(log, initializerCfg, mcc, mgr). - SetupWithManager(mgr, defaultCfg, predicate.Not(predicates.IsAccountTypeOrg())); err != nil { + SetupWithManager(mgr, defaultCfg, predicate.Not(predicates.LogicalClusterIsAccountTypeOrg())); err != nil { setupLog.Error(err, "unable to create controller", "controller", "AccountLogicalCluster") os.Exit(1) } diff --git a/internal/predicates/accounttype.go b/internal/predicates/accounttype.go index 9789a263..dc67438a 100644 --- a/internal/predicates/accounttype.go +++ b/internal/predicates/accounttype.go @@ -1,19 +1,26 @@ package predicates import ( + "strings" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/predicate" ) -// IsAccountTypeOrg returns a predicate that filters for LogicalClusters -// belonging to an Account of type "org". -// todo(simontesar): more stable implementation not relying on static orgs path -func IsAccountTypeOrg() predicate.Predicate { +const kcpPathAnnotation = "kcp.io/path" + +// LogicalClusterIsAccountTypeOrg returns a predicate that filters for +// LogicalClusters belonging to an Account of type "org", i.e. is a child of the +// "root:orgs" cluster. +func LogicalClusterIsAccountTypeOrg() predicate.Predicate { return predicate.NewPredicateFuncs(func(object client.Object) bool { lc := object.(*kcpcorev1alpha1.LogicalCluster) + p := lc.Annotations[kcpPathAnnotation] + + parts := strings.Split(p, ":") - return lc.Spec.Owner.Name == "orgs" + return parts[0] == "root" && parts[1] == "orgs" && len(parts) == 3 }) } From 341aeb4cfede3967224a3421eb3aaf174926b477 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Thu, 5 Feb 2026 12:36:29 +0000 Subject: [PATCH 10/61] fix: invite: clearer and non-duplicate error messages On-behalf-of: @SAP --- internal/subroutine/invite/subroutine.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/subroutine/invite/subroutine.go b/internal/subroutine/invite/subroutine.go index c9f66f3a..97a2acd1 100644 --- a/internal/subroutine/invite/subroutine.go +++ b/internal/subroutine/invite/subroutine.go @@ -214,13 +214,12 @@ func (s *subroutine) Process(ctx context.Context, instance runtimeobject.Runtime res, err = s.keycloak.Post(fmt.Sprintf("%s/admin/realms/%s/users", s.keycloakBaseURL, realm), "application/json", &buffer) if err != nil { // coverage-ignore - log.Err(err).Msg("Failed to create user") - return ctrl.Result{}, errors.NewOperatorError(err, true, true) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("posting to Keycloak to create user: %w", err), true, true) } defer res.Body.Close() //nolint:errcheck if res.StatusCode != http.StatusCreated { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("failed to create user: %s", res.Status), true, true) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("keylcloak returned non-200 status code: %s", res.Status), true, true) } res, err = s.keycloak.Get(fmt.Sprintf("%s/admin/realms/%s/users?%s", s.keycloakBaseURL, realm, v.Encode())) From 1b4bb234769514df6a083971990f596cf227eb8c Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Mon, 9 Feb 2026 05:03:04 +0000 Subject: [PATCH 11/61] feat: introduce client package On-behalf-of: @SAP --- internal/client/all_platformmesh.go | 46 +++++++++++++++++++++++++++ internal/client/logicalcluster.go | 34 ++++++++++++++++++++ internal/subroutine/account_tuples.go | 38 ++++++---------------- 3 files changed, 89 insertions(+), 29 deletions(-) create mode 100644 internal/client/all_platformmesh.go create mode 100644 internal/client/logicalcluster.go diff --git a/internal/client/all_platformmesh.go b/internal/client/all_platformmesh.go new file mode 100644 index 00000000..3e34ffa3 --- /dev/null +++ b/internal/client/all_platformmesh.go @@ -0,0 +1,46 @@ +package client + +import ( + "context" + "fmt" + "net/url" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + + "github.com/kcp-dev/logicalcluster/v3" + kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" +) + +// todo(simontesar): what is the actual source of truth for these? +const ( + corePlatformMeshIOAPIExportEndpointSlice = "core.platform-mesh.io" + platformMeshSystemWorkspace = "root:platform-mesh-system" +) + +func NewForAllPlatformMeshResources(ctx context.Context, config *rest.Config, scheme *runtime.Scheme) (client.Client, error) { + platformMeshClient, err := NewForLogicalCluster(config, scheme, logicalcluster.Name(platformMeshSystemWorkspace)) + if err != nil { + return nil, fmt.Errorf("creating %s client: %w", platformMeshSystemWorkspace, err) + } + + var apiExportEndpointSlice kcpapisv1alpha1.APIExportEndpointSlice + if err := platformMeshClient.Get(ctx, types.NamespacedName{Name: corePlatformMeshIOAPIExportEndpointSlice}, &apiExportEndpointSlice); err != nil { + return nil, fmt.Errorf("getting %s APIExportEndpointSlice: %w", corePlatformMeshIOAPIExportEndpointSlice, err) + } + + virtualWorkspaceUrl, err := url.Parse(apiExportEndpointSlice.Status.APIExportEndpoints[0].URL) + if err != nil { + return nil, fmt.Errorf("parsing virtual workspace URL: %w", err) + } + + path, err := url.JoinPath(virtualWorkspaceUrl.Path, "clusters", logicalcluster.Wildcard.String()) + if err != nil { + return nil, fmt.Errorf("joining path: %w", err) + } + + return clientForPath(config, scheme, path) +} diff --git a/internal/client/logicalcluster.go b/internal/client/logicalcluster.go new file mode 100644 index 00000000..e14643ea --- /dev/null +++ b/internal/client/logicalcluster.go @@ -0,0 +1,34 @@ +package client + +import ( + "fmt" + "net/url" + + "github.com/kcp-dev/logicalcluster/v3" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func NewForLogicalCluster(config *rest.Config, scheme *runtime.Scheme, clusterKey logicalcluster.Name) (client.Client, error) { + path := fmt.Sprintf("/clusters/%s", clusterKey) + + return clientForPath(config, scheme, path) +} + +func clientForPath(config *rest.Config, scheme *runtime.Scheme, path string) (client.Client, error) { + copy := rest.CopyConfig(config) + + parsed, err := url.Parse(copy.Host) + if err != nil { + return nil, fmt.Errorf("parsing host from config: %w", err) + } + parsed.Path = path + copy.Host = parsed.String() + + fmt.Printf("Creating client for host %s\n", copy.Host) + + return client.New(copy, client.Options{ + Scheme: scheme, + }) +} diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index f65e72c4..815cf6f0 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -3,7 +3,6 @@ package subroutine import ( "context" "fmt" - "net/url" "slices" "strings" @@ -11,10 +10,7 @@ import ( mcclient "github.com/kcp-dev/multicluster-provider/client" kcpcore "github.com/kcp-dev/sdk/apis/core" kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" - "github.com/rs/zerolog/log" kerrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/rest" accountsv1alpha1 "github.com/platform-mesh/account-operator/api/v1alpha1" "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" @@ -22,6 +18,8 @@ import ( "github.com/platform-mesh/golang-commons/errors" "github.com/platform-mesh/golang-commons/logger" "github.com/platform-mesh/security-operator/api/v1alpha1" + iclient "github.com/platform-mesh/security-operator/internal/client" + logicalclusterclient "github.com/platform-mesh/security-operator/internal/client" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" @@ -50,7 +48,7 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo log = log.ChildLogger("ID", lcID).ChildLogger("path", p) log.Info().Msgf("Processing logical cluster") - lcClient, err := NewLCClient(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(lcID)) + lcClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(lcID)) if err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting client: %w", err), true, true) } @@ -66,7 +64,7 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("AccountInfo not found yet, requeueing"), true, false) } - parentOrgClient, err := NewLCClient(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(ai.Spec.Organization.Path)) + parentOrgClient, err := logicalclusterclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(ai.Spec.Organization.Path)) if err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting parent organisation client: %w", err), true, true) } @@ -78,7 +76,7 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Account in parent organisation: %w", err), true, true) } - orgsClient, err := NewLCClient(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name("root:orgs")) + orgsClient, err := logicalclusterclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name("root:orgs")) if err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting orgs client: %w", err), true, true) } @@ -119,13 +117,13 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo if err := orgsClient.Update(ctx, &st); err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("updating Store with tuples: %w", err), true, true) } + if err := orgsClient.Get(ctx, client.ObjectKey{Name: st.Name}, &st); err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Store after update: %w", err), true, true) + } // todo(simontesar): checking and waiting for Readiness is currently futile // our conditions don't include the observed generation - // - // if err := orgsClient.Get(ctx, client.ObjectKey{Name: st.Name}, &st); err != nil { - // return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Store after update: %w", err), true, true) - // } + // if conditions.IsPresentAndEqualForGeneration(st.Status.Conditions, lcconditions.ConditionReady, metav1.ConditionTrue, st.GetObjectMeta().GetGeneration()) { // return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("store %s is not ready", st.Name), true, false) // } @@ -167,24 +165,6 @@ func NewAccountTuplesSubroutine(mcc mcclient.ClusterClient, mgr mcmanager.Manage var _ lifecyclesubroutine.Subroutine = &AccountTuplesSubroutine{} -func NewLCClient(config *rest.Config, scheme *runtime.Scheme, clusterKey logicalcluster.Name) (client.Client, error) { - cfg := rest.CopyConfig(config) - - parsed, err := url.Parse(cfg.Host) - if err != nil { - log.Error().Err(err).Msg("unable to parse host") - panic(err) - } - - parsed.Path = fmt.Sprintf("/clusters/%s", clusterKey) - - cfg.Host = parsed.String() - - return client.New(cfg, client.Options{ - Scheme: scheme, - }) -} - // isServiceAccount determines wheter a user appears to be a Kubernetes // ServiceAccount. func isServiceAccount(user string) bool { From f18194ac23403a28fc7035a30d4c9a6c5810b218 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Mon, 9 Feb 2026 05:06:15 +0000 Subject: [PATCH 12/61] feat: account-initializer: check tuples in status On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index 815cf6f0..e25b520f 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -121,6 +121,14 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Store after update: %w", err), true, true) } + for _, t := range tuples { + if !slices.Contains(st.Status.ManagedTuples, t) { + log.Info().Msgf("Store does not yet contain all specified tuples, requeueing") + // todo: add watch instead of requeue + return ctrl.Result{Requeue: true}, nil + } + } + // todo(simontesar): checking and waiting for Readiness is currently futile // our conditions don't include the observed generation From ac4d5eda4a24809a234577a30b9fabd6c8f00620 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Mon, 9 Feb 2026 07:25:20 +0000 Subject: [PATCH 13/61] chore: account-initializer: reordering and comments On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index e25b520f..dd43ef02 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -88,6 +88,7 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting parent organisation's Store: %w", err), true, true) } + // Build tuples for Account tuples := []v1alpha1.Tuple{ v1alpha1.Tuple{ User: fmt.Sprintf("%s:%s/%s", s.objectType, ai.Spec.ParentAccount.OriginClusterId, ai.Spec.ParentAccount.Name), @@ -117,21 +118,22 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo if err := orgsClient.Update(ctx, &st); err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("updating Store with tuples: %w", err), true, true) } + // Re-get Store for potential update if err := orgsClient.Get(ctx, client.ObjectKey{Name: st.Name}, &st); err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Store after update: %w", err), true, true) } + // Check if Store applied tuple changes for _, t := range tuples { if !slices.Contains(st.Status.ManagedTuples, t) { log.Info().Msgf("Store does not yet contain all specified tuples, requeueing") - // todo: add watch instead of requeue + return ctrl.Result{Requeue: true}, nil } } - // todo(simontesar): checking and waiting for Readiness is currently futile + // todo(simontesar): checking and waiting for Readiness is currently futile, // our conditions don't include the observed generation - // if conditions.IsPresentAndEqualForGeneration(st.Status.Conditions, lcconditions.ConditionReady, metav1.ConditionTrue, st.GetObjectMeta().GetGeneration()) { // return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("store %s is not ready", st.Name), true, false) // } From 642d28adfe69bbc1201c3246464d9aef5147855a Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Mon, 9 Feb 2026 08:24:00 +0000 Subject: [PATCH 14/61] refactor: rename org initalization controller On-behalf-of: @SAP --- cmd/initializer.go | 2 +- internal/controller/orglogicalcluster_controller.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/initializer.go b/cmd/initializer.go index 3c866b84..a114602b 100644 --- a/cmd/initializer.go +++ b/cmd/initializer.go @@ -97,7 +97,7 @@ var initializerCmd = &cobra.Command{ initializerCfg.IDP.AdditionalRedirectURLs = []string{} } - if err := controller.NewLogicalClusterReconciler(log, orgClient, initializerCfg, runtimeClient, mgr). + if err := controller.NewOrgLogicalClusterReconciler(log, orgClient, initializerCfg, runtimeClient, mgr). SetupWithManager(mgr, defaultCfg, predicates.LogicalClusterIsAccountTypeOrg()); err != nil { setupLog.Error(err, "unable to create controller", "controller", "LogicalCluster") os.Exit(1) diff --git a/internal/controller/orglogicalcluster_controller.go b/internal/controller/orglogicalcluster_controller.go index 8a77a2f9..ce4128c9 100644 --- a/internal/controller/orglogicalcluster_controller.go +++ b/internal/controller/orglogicalcluster_controller.go @@ -20,14 +20,14 @@ import ( kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" ) -type LogicalClusterReconciler struct { +type OrgLogicalClusterReconciler struct { log *logger.Logger mclifecycle *multicluster.LifecycleManager } -func NewLogicalClusterReconciler(log *logger.Logger, orgClient client.Client, cfg config.Config, inClusterClient client.Client, mgr mcmanager.Manager) *LogicalClusterReconciler { - return &LogicalClusterReconciler{ +func NewOrgLogicalClusterReconciler(log *logger.Logger, orgClient client.Client, cfg config.Config, inClusterClient client.Client, mgr mcmanager.Manager) *OrgLogicalClusterReconciler { + return &OrgLogicalClusterReconciler{ log: log, mclifecycle: builder.NewBuilder("logicalcluster", "LogicalClusterReconciler", []lifecyclesubroutine.Subroutine{ subroutine.NewWorkspaceInitializer(orgClient, cfg, mgr), @@ -42,11 +42,11 @@ func NewLogicalClusterReconciler(log *logger.Logger, orgClient client.Client, cf } } -func (r *LogicalClusterReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) { +func (r *OrgLogicalClusterReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) { ctxWithCluster := mccontext.WithCluster(ctx, req.ClusterName) return r.mclifecycle.Reconcile(ctxWithCluster, req, &kcpcorev1alpha1.LogicalCluster{}) } -func (r *LogicalClusterReconciler) SetupWithManager(mgr mcmanager.Manager, cfg *platformeshconfig.CommonServiceConfig, evp ...predicate.Predicate) error { +func (r *OrgLogicalClusterReconciler) SetupWithManager(mgr mcmanager.Manager, cfg *platformeshconfig.CommonServiceConfig, evp ...predicate.Predicate) error { return r.mclifecycle.SetupWithManager(mgr, cfg.MaxConcurrentReconciles, "LogicalCluster", &kcpcorev1alpha1.LogicalCluster{}, cfg.DebugLabelValue, r, r.log, evp...) } From 4ca56ae8ec868b4771ddd3e3c817b8b01ec006af Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 10 Feb 2026 06:33:33 +0000 Subject: [PATCH 15/61] feat: add internal fga package On-behalf-of: @SAP --- internal/fga/tuples.go | 55 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 internal/fga/tuples.go diff --git a/internal/fga/tuples.go b/internal/fga/tuples.go new file mode 100644 index 00000000..82e09279 --- /dev/null +++ b/internal/fga/tuples.go @@ -0,0 +1,55 @@ +package fga + +import ( + "fmt" + "strings" + + accountv1alpha1 "github.com/platform-mesh/account-operator/api/v1alpha1" + "github.com/platform-mesh/security-operator/api/v1alpha1" +) + +func TuplesForAccount(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInfo, creatorRelation, parentRelation, objectType string) []v1alpha1.Tuple { + tuples := append(baseTuples(acc, ai, creatorRelation, objectType), v1alpha1.Tuple{ + User: fmt.Sprintf("%s:%s/%s", objectType, ai.Spec.ParentAccount.OriginClusterId, ai.Spec.ParentAccount.Name), + Relation: parentRelation, + Object: fmt.Sprintf("%s:%s/%s", objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), + }) + + return tuples +} + +func TuplesForOrganization(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInfo, creatorRelation, objectType string) []v1alpha1.Tuple { + return baseTuples(acc, ai, creatorRelation, objectType) +} + +func baseTuples(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInfo, creatorRelation, objectType string) []v1alpha1.Tuple { + return []v1alpha1.Tuple{ + v1alpha1.Tuple{ + User: fmt.Sprintf("user:%s", formatUser(*acc.Spec.Creator)), + Relation: "assignee", + Object: fmt.Sprintf("role:%s/%s/%s/owner", objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), + }, + v1alpha1.Tuple{ + User: fmt.Sprintf("role:%s/%s/%s/owner#assignee", objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), + Relation: creatorRelation, + Object: fmt.Sprintf("%s:%s/%s", objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), + }, + } +} + +// formatUser formats a user to be stored in an FGA tuple, i.e. replaces colons +// with dots in case of a Kubernetes ServiceAccount. +// todo(simontesar): why was this implemented ot only be done in case of SAs? +func formatUser(user string) string { + if isServiceAccount(user) { + return strings.ReplaceAll(user, ":", ".") + } + + return user +} + +// isServiceAccount determines wheter a user appears to be a Kubernetes +// ServiceAccount. +func isServiceAccount(user string) bool { + return strings.HasPrefix(user, "system:serviceaccount:") +} From d3a5350e7babee4a25c69d65fa7463f16943c714 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 10 Feb 2026 06:35:19 +0000 Subject: [PATCH 16/61] fix: logicalclusterclient: remove bogus print On-behalf-of: @SAP --- internal/client/logicalcluster.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/client/logicalcluster.go b/internal/client/logicalcluster.go index e14643ea..748e6620 100644 --- a/internal/client/logicalcluster.go +++ b/internal/client/logicalcluster.go @@ -26,8 +26,6 @@ func clientForPath(config *rest.Config, scheme *runtime.Scheme, path string) (cl parsed.Path = path copy.Host = parsed.String() - fmt.Printf("Creating client for host %s\n", copy.Host) - return client.New(copy, client.Options{ Scheme: scheme, }) From 4fe4960ee9b8f32ab45a17d772d58f6ccbb42fb5 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 10 Feb 2026 06:36:19 +0000 Subject: [PATCH 17/61] feat: account-tuples: check for change On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 42 ++++++++------------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index dd43ef02..a413bd56 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -20,6 +20,7 @@ import ( "github.com/platform-mesh/security-operator/api/v1alpha1" iclient "github.com/platform-mesh/security-operator/internal/client" logicalclusterclient "github.com/platform-mesh/security-operator/internal/client" + "github.com/platform-mesh/security-operator/internal/fga" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" @@ -59,8 +60,6 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo }, &ai); err != nil && !kerrors.IsNotFound(err) { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting AccountInfo for LogicalCluster: %w", err), true, true) } else if kerrors.IsNotFound(err) { - fmt.Println(err) - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("AccountInfo not found yet, requeueing"), true, false) } @@ -88,47 +87,30 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting parent organisation's Store: %w", err), true, true) } - // Build tuples for Account - tuples := []v1alpha1.Tuple{ - v1alpha1.Tuple{ - User: fmt.Sprintf("%s:%s/%s", s.objectType, ai.Spec.ParentAccount.OriginClusterId, ai.Spec.ParentAccount.Name), - Relation: s.parentRelation, - Object: fmt.Sprintf("%s:%s/%s", s.objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), - }, - v1alpha1.Tuple{ - User: fmt.Sprintf("user:%s", formatUser(*acc.Spec.Creator)), - Relation: "assignee", - Object: fmt.Sprintf("role:%s/%s/%s/owner", s.objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), - }, - v1alpha1.Tuple{ - User: fmt.Sprintf("role:%s/%s/%s/owner#assignee", s.objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), - Relation: s.creatorRelation, - Object: fmt.Sprintf("%s:%s/%s", s.objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), - }, - } - // Append the stores tuples with every tuple for the Account not yet managed // via the Store resource + tuples := fga.TuplesForAccount(acc, ai, s.creatorRelation, s.parentRelation, s.objectType) + var changed bool for _, t := range tuples { if !slices.Contains(st.Spec.Tuples, t) { st.Spec.Tuples = append(st.Spec.Tuples, t) + changed = true } } - if err := orgsClient.Update(ctx, &st); err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("updating Store with tuples: %w", err), true, true) - } - // Re-get Store for potential update - if err := orgsClient.Get(ctx, client.ObjectKey{Name: st.Name}, &st); err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Store after update: %w", err), true, true) + if changed { + if err := orgsClient.Update(ctx, &st); err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("updating Store with tuples: %w", err), true, true) + } + + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("Store needed to be updated, requeueing"), true, false) } + fmt.Printf("Checking tuples: %v", st.Status.ManagedTuples) // Check if Store applied tuple changes for _, t := range tuples { if !slices.Contains(st.Status.ManagedTuples, t) { - log.Info().Msgf("Store does not yet contain all specified tuples, requeueing") - - return ctrl.Result{Requeue: true}, nil + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("Store does not yet contain all specified tuples, requeueing"), true, false) } } From ae12f7b6e017f6d323dc677fdb316f9bb424ff4a Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 10 Feb 2026 06:37:25 +0000 Subject: [PATCH 18/61] feat: org-initializer: create tuples On-behalf-of: @SAP --- .../orglogicalcluster_controller.go | 4 +- internal/subroutine/workspace_initializer.go | 58 +++++++++++++++++-- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/internal/controller/orglogicalcluster_controller.go b/internal/controller/orglogicalcluster_controller.go index ce4128c9..f73e3a5a 100644 --- a/internal/controller/orglogicalcluster_controller.go +++ b/internal/controller/orglogicalcluster_controller.go @@ -29,8 +29,8 @@ type OrgLogicalClusterReconciler struct { func NewOrgLogicalClusterReconciler(log *logger.Logger, orgClient client.Client, cfg config.Config, inClusterClient client.Client, mgr mcmanager.Manager) *OrgLogicalClusterReconciler { return &OrgLogicalClusterReconciler{ log: log, - mclifecycle: builder.NewBuilder("logicalcluster", "LogicalClusterReconciler", []lifecyclesubroutine.Subroutine{ - subroutine.NewWorkspaceInitializer(orgClient, cfg, mgr), + mclifecycle: builder.NewBuilder("security", "OrgLogicalClusterReconciler", []lifecyclesubroutine.Subroutine{ + subroutine.NewWorkspaceInitializer(orgClient, cfg, mgr, cfg.FGA.CreatorRelation, cfg.FGA.ParentRelation, cfg.FGA.ObjectType), subroutine.NewIDPSubroutine(orgClient, mgr, cfg), subroutine.NewInviteSubroutine(orgClient, mgr), subroutine.NewWorkspaceAuthConfigurationSubroutine(orgClient, inClusterClient, mgr, cfg), diff --git a/internal/subroutine/workspace_initializer.go b/internal/subroutine/workspace_initializer.go index 60c9b2a4..814d6ab9 100644 --- a/internal/subroutine/workspace_initializer.go +++ b/internal/subroutine/workspace_initializer.go @@ -10,19 +10,27 @@ import ( "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/errors" + "github.com/platform-mesh/golang-commons/logger" "github.com/platform-mesh/security-operator/api/v1alpha1" + iclient "github.com/platform-mesh/security-operator/internal/client" + logicalclusterclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/internal/config" + "github.com/platform-mesh/security-operator/internal/fga" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/kcp-dev/logicalcluster/v3" + kcpcore "github.com/kcp-dev/sdk/apis/core" kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" ) -func NewWorkspaceInitializer(orgsClient client.Client, cfg config.Config, mgr mcmanager.Manager) *workspaceInitializer { +func NewWorkspaceInitializer(orgsClient client.Client, cfg config.Config, mgr mcmanager.Manager, creatorRelation, parentRelation, objectType string) *workspaceInitializer { // read file from path res, err := os.ReadFile(cfg.CoreModulePath) if err != nil { @@ -35,6 +43,9 @@ func NewWorkspaceInitializer(orgsClient client.Client, cfg config.Config, mgr mc initializerName: cfg.InitializerName(), mgr: mgr, cfg: cfg, + creatorRelation: creatorRelation, + parentRelation: parentRelation, + objectType: objectType, } } @@ -46,6 +57,10 @@ type workspaceInitializer struct { cfg config.Config coreModule string initializerName string + + objectType string + parentRelation string + creatorRelation string } func (w *workspaceInitializer) Finalize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { @@ -60,16 +75,52 @@ func (w *workspaceInitializer) Finalizers(_ runtimeobject.RuntimeObject) []strin func (w *workspaceInitializer) GetName() string { return "WorkspaceInitializer" } func (w *workspaceInitializer) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + log := logger.LoadLoggerFromContext(ctx) + lc := instance.(*kcpcorev1alpha1.LogicalCluster) + p := lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] + if p == "" { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster is not set"), true, true) + } + lcID, _ := mccontext.ClusterFrom(ctx) + log = log.ChildLogger("ID", lcID).ChildLogger("path", p) + log.Info().Msgf("Processing logical cluster") + + lcClient, err := iclient.NewForLogicalCluster(w.mgr.GetLocalManager().GetConfig(), w.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(lcID)) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting client: %w", err), true, true) + } + + var ai accountsv1alpha1.AccountInfo + if err := lcClient.Get(ctx, client.ObjectKey{ + Name: accountsv1alpha1.DefaultAccountInfoName, + }, &ai); err != nil && !kerrors.IsNotFound(err) { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting AccountInfo for LogicalCluster: %w", err), true, true) + } else if kerrors.IsNotFound(err) { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("AccountInfo not found yet, requeueing"), true, false) + } + + orgsClient, err := logicalclusterclient.NewForLogicalCluster(w.mgr.GetLocalManager().GetConfig(), w.mgr.GetLocalManager().GetScheme(), logicalcluster.Name("root:orgs")) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting parent organisation client: %w", err), true, true) + } + + var acc accountsv1alpha1.Account + if err := orgsClient.Get(ctx, client.ObjectKey{ + Name: ai.Spec.Account.Name, + }, &acc); err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Account in platform-mesh-system: %w", err), true, true) + } store := v1alpha1.Store{ ObjectMeta: metav1.ObjectMeta{Name: generateStoreName(lc)}, } - _, err := controllerutil.CreateOrUpdate(ctx, w.orgsClient, &store, func() error { + if _, err := controllerutil.CreateOrUpdate(ctx, w.orgsClient, &store, func() error { store.Spec = v1alpha1.StoreSpec{ CoreModule: w.coreModule, } + store.Spec.Tuples = fga.TuplesForOrganization(acc, ai, w.creatorRelation, w.objectType) if w.cfg.AllowMemberTuplesEnabled { // TODO: remove this flag once the feature is tested and stable store.Spec.Tuples = []v1alpha1.Tuple{ @@ -87,8 +138,7 @@ func (w *workspaceInitializer) Process(ctx context.Context, instance runtimeobje } return nil - }) - if err != nil { + }); err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("unable to create/update store: %w", err), true, true) } From e650fb13020135b1e9b9fe4f257f44d00d0d7c98 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 10 Feb 2026 06:44:04 +0000 Subject: [PATCH 19/61] refactor: make fga package importable On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 20 +------------------- internal/subroutine/workspace_initializer.go | 2 +- {internal => pkg}/fga/tuples.go | 0 3 files changed, 2 insertions(+), 20 deletions(-) rename {internal => pkg}/fga/tuples.go (100%) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index a413bd56..232a9457 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "slices" - "strings" "github.com/kcp-dev/logicalcluster/v3" mcclient "github.com/kcp-dev/multicluster-provider/client" @@ -20,7 +19,7 @@ import ( "github.com/platform-mesh/security-operator/api/v1alpha1" iclient "github.com/platform-mesh/security-operator/internal/client" logicalclusterclient "github.com/platform-mesh/security-operator/internal/client" - "github.com/platform-mesh/security-operator/internal/fga" + "github.com/platform-mesh/security-operator/pkg/fga" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" @@ -156,20 +155,3 @@ func NewAccountTuplesSubroutine(mcc mcclient.ClusterClient, mgr mcmanager.Manage } var _ lifecyclesubroutine.Subroutine = &AccountTuplesSubroutine{} - -// isServiceAccount determines wheter a user appears to be a Kubernetes -// ServiceAccount. -func isServiceAccount(user string) bool { - return strings.HasPrefix(user, "system:serviceaccount:") -} - -// formatUser formats a user to be stored in an FGA tuple, i.e. replaces colons -// with dots in case of a Kubernetes ServiceAccount. -// todo(simontesar): why was this implemented ot only be done in case of SAs? -func formatUser(user string) string { - if isServiceAccount(user) { - return strings.ReplaceAll(user, ":", ".") - } - - return user -} diff --git a/internal/subroutine/workspace_initializer.go b/internal/subroutine/workspace_initializer.go index 814d6ab9..caf8cadf 100644 --- a/internal/subroutine/workspace_initializer.go +++ b/internal/subroutine/workspace_initializer.go @@ -15,7 +15,7 @@ import ( iclient "github.com/platform-mesh/security-operator/internal/client" logicalclusterclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/internal/config" - "github.com/platform-mesh/security-operator/internal/fga" + "github.com/platform-mesh/security-operator/pkg/fga" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" diff --git a/internal/fga/tuples.go b/pkg/fga/tuples.go similarity index 100% rename from internal/fga/tuples.go rename to pkg/fga/tuples.go From f4cc794bf661a3645a713b5b1744a938e7cb0cad Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 10 Feb 2026 09:38:40 +0000 Subject: [PATCH 20/61] fix: remove bogus print On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index 232a9457..3aec9d7a 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -105,7 +105,6 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("Store needed to be updated, requeueing"), true, false) } - fmt.Printf("Checking tuples: %v", st.Status.ManagedTuples) // Check if Store applied tuple changes for _, t := range tuples { if !slices.Contains(st.Status.ManagedTuples, t) { From 4c13c3ad8471ad6e737205347816d2355fb0544b Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 10 Feb 2026 09:51:19 +0000 Subject: [PATCH 21/61] refactor: remove constant dependencies on account operator On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 2 +- internal/subroutine/workspace_initializer.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index 3aec9d7a..bb31c05d 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -55,7 +55,7 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo var ai accountsv1alpha1.AccountInfo if err := lcClient.Get(ctx, client.ObjectKey{ - Name: accountsv1alpha1.DefaultAccountInfoName, + Name: "account", }, &ai); err != nil && !kerrors.IsNotFound(err) { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting AccountInfo for LogicalCluster: %w", err), true, true) } else if kerrors.IsNotFound(err) { diff --git a/internal/subroutine/workspace_initializer.go b/internal/subroutine/workspace_initializer.go index caf8cadf..cb370755 100644 --- a/internal/subroutine/workspace_initializer.go +++ b/internal/subroutine/workspace_initializer.go @@ -93,7 +93,7 @@ func (w *workspaceInitializer) Process(ctx context.Context, instance runtimeobje var ai accountsv1alpha1.AccountInfo if err := lcClient.Get(ctx, client.ObjectKey{ - Name: accountsv1alpha1.DefaultAccountInfoName, + Name: "account", }, &ai); err != nil && !kerrors.IsNotFound(err) { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting AccountInfo for LogicalCluster: %w", err), true, true) } else if kerrors.IsNotFound(err) { From 519c56c6c54409d251520130fe59057a9567835b Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 10 Feb 2026 10:04:11 +0000 Subject: [PATCH 22/61] chore: some comments On-behalf-of: @SAP --- internal/client/all_platformmesh.go | 4 +++- internal/client/logicalcluster.go | 3 +++ internal/controller/accountlogicalcluster_controller.go | 1 + internal/subroutine/account_tuples.go | 2 ++ pkg/fga/tuples.go | 2 ++ 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/client/all_platformmesh.go b/internal/client/all_platformmesh.go index 3e34ffa3..5b1b39de 100644 --- a/internal/client/all_platformmesh.go +++ b/internal/client/all_platformmesh.go @@ -15,12 +15,14 @@ import ( kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" ) -// todo(simontesar): what is the actual source of truth for these? const ( corePlatformMeshIOAPIExportEndpointSlice = "core.platform-mesh.io" platformMeshSystemWorkspace = "root:platform-mesh-system" ) +// NewForAllPlatformMeshResources returns a client that can query all resources +// of the core.platform-mesh.io APIExportEndpoint slice, based on a given KCP +// base config. func NewForAllPlatformMeshResources(ctx context.Context, config *rest.Config, scheme *runtime.Scheme) (client.Client, error) { platformMeshClient, err := NewForLogicalCluster(config, scheme, logicalcluster.Name(platformMeshSystemWorkspace)) if err != nil { diff --git a/internal/client/logicalcluster.go b/internal/client/logicalcluster.go index 748e6620..618bcf17 100644 --- a/internal/client/logicalcluster.go +++ b/internal/client/logicalcluster.go @@ -10,12 +10,15 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +// NewForLogicalCluster returns a client for a given logical cluster name or +// path, based on a KCP base config. func NewForLogicalCluster(config *rest.Config, scheme *runtime.Scheme, clusterKey logicalcluster.Name) (client.Client, error) { path := fmt.Sprintf("/clusters/%s", clusterKey) return clientForPath(config, scheme, path) } +// clientForPath returns a client for a give raw URL path. func clientForPath(config *rest.Config, scheme *runtime.Scheme, path string) (client.Client, error) { copy := rest.CopyConfig(config) diff --git a/internal/controller/accountlogicalcluster_controller.go b/internal/controller/accountlogicalcluster_controller.go index f68812e6..093c8ff7 100644 --- a/internal/controller/accountlogicalcluster_controller.go +++ b/internal/controller/accountlogicalcluster_controller.go @@ -20,6 +20,7 @@ import ( kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" ) +// AccountLogicalClusterReconciler acts as an intializer for account workspaces. type AccountLogicalClusterReconciler struct { log *logger.Logger diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index bb31c05d..71159233 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -26,6 +26,8 @@ import ( mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" ) +// AccountTuplesSubroutine creates FGA tuples for Accounts not of the +// "org"-type. type AccountTuplesSubroutine struct { mgr mcmanager.Manager mcc mcclient.ClusterClient diff --git a/pkg/fga/tuples.go b/pkg/fga/tuples.go index 82e09279..04a4ff56 100644 --- a/pkg/fga/tuples.go +++ b/pkg/fga/tuples.go @@ -8,6 +8,7 @@ import ( "github.com/platform-mesh/security-operator/api/v1alpha1" ) +// TuplesForAccount returns FGA tuples for an account not of type organization. func TuplesForAccount(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInfo, creatorRelation, parentRelation, objectType string) []v1alpha1.Tuple { tuples := append(baseTuples(acc, ai, creatorRelation, objectType), v1alpha1.Tuple{ User: fmt.Sprintf("%s:%s/%s", objectType, ai.Spec.ParentAccount.OriginClusterId, ai.Spec.ParentAccount.Name), @@ -18,6 +19,7 @@ func TuplesForAccount(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInf return tuples } +// TuplesForOrganization returns FGA tuples for an Account of type organization. func TuplesForOrganization(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInfo, creatorRelation, objectType string) []v1alpha1.Tuple { return baseTuples(acc, ai, creatorRelation, objectType) } From 6fadccecc5d0ca6e1edbf8fa2cdadd355e08c978 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 10 Feb 2026 10:04:23 +0000 Subject: [PATCH 23/61] chore: go mod tidy On-behalf-of: @SAP --- go.mod | 16 ++++++++-------- go.sum | 38 ++++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index 8195e088..33a8b023 100644 --- a/go.mod +++ b/go.mod @@ -93,27 +93,27 @@ require ( github.com/vektah/gqlparser/v2 v2.5.31 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect - go.opentelemetry.io/otel/metric v1.39.0 // indirect - go.opentelemetry.io/otel/sdk v1.39.0 // indirect - go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.46.0 // indirect + golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/net v0.49.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect - golang.org/x/term v0.38.0 // indirect + golang.org/x/term v0.39.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.11.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index a9183e8d..519fd8bc 100644 --- a/go.sum +++ b/go.sum @@ -151,8 +151,6 @@ github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/openfga/api/proto v0.0.0-20260122181957-618e7e0a4878 h1:BAg/v38U3stiKx/po8k8F5Sgt4U8KP3+1jBt/aKQMsI= github.com/openfga/api/proto v0.0.0-20260122181957-618e7e0a4878/go.mod h1:XDX4qYNBUM2Rsa2AbKPh+oocZc2zgme+EF2fFC6amVU= -github.com/openfga/go-sdk v0.7.4 h1:WBZDjl5Aqy1pFsDCL9LGZ5teJsYh42giFWA7G4AHfkw= -github.com/openfga/go-sdk v0.7.4/go.mod h1:jGyDrPZauqrGM89iSqvjVwwF80fKCTOIERGZ+X3H4pI= github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c h1:xPbHNFG8QbPr/fpL7u0MPI0x74/BCLm7Sx02btL1m5Q= github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c/go.mod h1:BG26d1Fk4GSg0wMj60TRJ6Pe4ka2WQ33akhO+mzt3t0= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -221,22 +219,22 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 h1:8UPA4IbVZxpsD76ihGOQiFml99GPAEZLohDXvqHdi6U= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0/go.mod h1:MZ1T/+51uIVKlRzGw1Fo46KEWThjlCBZKl2LzY5nv4g= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.1-0.20241121203838-4ff5fa6529ee h1:uOMbcH1Dmxv45VkkpZQYoerZFeDncWpjbN7ATiQOO7c= @@ -249,14 +247,14 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= @@ -266,8 +264,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= @@ -280,8 +278,8 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E= google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= From b42e6a219cb0f7366cc3f44d490b2fddc9e620d3 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 10 Feb 2026 10:07:22 +0000 Subject: [PATCH 24/61] chore: comment about requeueing and updating On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index 71159233..09c59e6a 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -99,6 +99,8 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo } } + // Potentially update Store and requeue to wait for update + // todo(simontesar): we could be watching Stores instead if changed { if err := orgsClient.Update(ctx, &st); err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("updating Store with tuples: %w", err), true, true) From 046b7e8a5c00a3cd2f3dd343fde84957c3f0fc4c Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 10 Feb 2026 12:24:13 +0000 Subject: [PATCH 25/61] fga: dont check for k8s SA but always replace colons with dots On-behalf-of: @SAP --- pkg/fga/tuples.go | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/pkg/fga/tuples.go b/pkg/fga/tuples.go index 04a4ff56..649c7d25 100644 --- a/pkg/fga/tuples.go +++ b/pkg/fga/tuples.go @@ -40,18 +40,7 @@ func baseTuples(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInfo, cre } // formatUser formats a user to be stored in an FGA tuple, i.e. replaces colons -// with dots in case of a Kubernetes ServiceAccount. -// todo(simontesar): why was this implemented ot only be done in case of SAs? +// with dots. func formatUser(user string) string { - if isServiceAccount(user) { - return strings.ReplaceAll(user, ":", ".") - } - - return user -} - -// isServiceAccount determines wheter a user appears to be a Kubernetes -// ServiceAccount. -func isServiceAccount(user string) bool { - return strings.HasPrefix(user, "system:serviceaccount:") + return strings.ReplaceAll(user, ":", ".") } From 23bf1b1397123b219c3609f19671a98fd96c3acd Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 10 Feb 2026 12:46:46 +0000 Subject: [PATCH 26/61] chore: formatting for linter On-behalf-of: @SAP --- cmd/initializer.go | 2 +- internal/client/logicalcluster.go | 6 +++-- .../accountlogicalcluster_controller.go | 2 +- internal/predicates/accounttype.go | 4 ++-- internal/subroutine/account_tuples.go | 22 +++++++++---------- internal/subroutine/workspace_initializer.go | 3 +-- pkg/fga/tuples.go | 4 ++-- 7 files changed, 22 insertions(+), 21 deletions(-) diff --git a/cmd/initializer.go b/cmd/initializer.go index a114602b..a1fefe4b 100644 --- a/cmd/initializer.go +++ b/cmd/initializer.go @@ -6,7 +6,6 @@ import ( helmv2 "github.com/fluxcd/helm-controller/api/v2" sourcev1 "github.com/fluxcd/source-controller/api/v1" - mcclient "github.com/kcp-dev/multicluster-provider/client" "github.com/platform-mesh/security-operator/internal/controller" "github.com/platform-mesh/security-operator/internal/predicates" "github.com/spf13/cobra" @@ -22,6 +21,7 @@ import ( "k8s.io/client-go/rest" "github.com/kcp-dev/logicalcluster/v3" + mcclient "github.com/kcp-dev/multicluster-provider/client" "github.com/kcp-dev/multicluster-provider/initializingworkspaces" ) diff --git a/internal/client/logicalcluster.go b/internal/client/logicalcluster.go index 618bcf17..72c951f1 100644 --- a/internal/client/logicalcluster.go +++ b/internal/client/logicalcluster.go @@ -4,10 +4,12 @@ import ( "fmt" "net/url" - "github.com/kcp-dev/logicalcluster/v3" + "sigs.k8s.io/controller-runtime/pkg/client" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kcp-dev/logicalcluster/v3" ) // NewForLogicalCluster returns a client for a given logical cluster name or diff --git a/internal/controller/accountlogicalcluster_controller.go b/internal/controller/accountlogicalcluster_controller.go index 093c8ff7..172beec0 100644 --- a/internal/controller/accountlogicalcluster_controller.go +++ b/internal/controller/accountlogicalcluster_controller.go @@ -3,7 +3,6 @@ package controller import ( "context" - mcclient "github.com/kcp-dev/multicluster-provider/client" platformeshconfig "github.com/platform-mesh/golang-commons/config" "github.com/platform-mesh/golang-commons/controller/lifecycle/builder" "github.com/platform-mesh/golang-commons/controller/lifecycle/multicluster" @@ -17,6 +16,7 @@ import ( mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + mcclient "github.com/kcp-dev/multicluster-provider/client" kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" ) diff --git a/internal/predicates/accounttype.go b/internal/predicates/accounttype.go index dc67438a..7dd1b396 100644 --- a/internal/predicates/accounttype.go +++ b/internal/predicates/accounttype.go @@ -3,10 +3,10 @@ package predicates import ( "strings" - kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/predicate" + + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" ) const kcpPathAnnotation = "kcp.io/path" diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index 09c59e6a..f4be2cca 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -5,12 +5,6 @@ import ( "fmt" "slices" - "github.com/kcp-dev/logicalcluster/v3" - mcclient "github.com/kcp-dev/multicluster-provider/client" - kcpcore "github.com/kcp-dev/sdk/apis/core" - kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" - kerrors "k8s.io/apimachinery/pkg/api/errors" - accountsv1alpha1 "github.com/platform-mesh/account-operator/api/v1alpha1" "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" @@ -18,12 +12,18 @@ import ( "github.com/platform-mesh/golang-commons/logger" "github.com/platform-mesh/security-operator/api/v1alpha1" iclient "github.com/platform-mesh/security-operator/internal/client" - logicalclusterclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/pkg/fga" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + kerrors "k8s.io/apimachinery/pkg/api/errors" + + "github.com/kcp-dev/logicalcluster/v3" + mcclient "github.com/kcp-dev/multicluster-provider/client" + kcpcore "github.com/kcp-dev/sdk/apis/core" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" ) // AccountTuplesSubroutine creates FGA tuples for Accounts not of the @@ -64,7 +64,7 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("AccountInfo not found yet, requeueing"), true, false) } - parentOrgClient, err := logicalclusterclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(ai.Spec.Organization.Path)) + parentOrgClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(ai.Spec.Organization.Path)) if err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting parent organisation client: %w", err), true, true) } @@ -76,7 +76,7 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Account in parent organisation: %w", err), true, true) } - orgsClient, err := logicalclusterclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name("root:orgs")) + orgsClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name("root:orgs")) if err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting orgs client: %w", err), true, true) } @@ -106,13 +106,13 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("updating Store with tuples: %w", err), true, true) } - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("Store needed to be updated, requeueing"), true, false) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("store needed to be updated, requeueing"), true, false) } // Check if Store applied tuple changes for _, t := range tuples { if !slices.Contains(st.Status.ManagedTuples, t) { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("Store does not yet contain all specified tuples, requeueing"), true, false) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("store does not yet contain all specified tuples, requeueing"), true, false) } } diff --git a/internal/subroutine/workspace_initializer.go b/internal/subroutine/workspace_initializer.go index cb370755..c7f1a5b2 100644 --- a/internal/subroutine/workspace_initializer.go +++ b/internal/subroutine/workspace_initializer.go @@ -13,7 +13,6 @@ import ( "github.com/platform-mesh/golang-commons/logger" "github.com/platform-mesh/security-operator/api/v1alpha1" iclient "github.com/platform-mesh/security-operator/internal/client" - logicalclusterclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/internal/config" "github.com/platform-mesh/security-operator/pkg/fga" ctrl "sigs.k8s.io/controller-runtime" @@ -100,7 +99,7 @@ func (w *workspaceInitializer) Process(ctx context.Context, instance runtimeobje return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("AccountInfo not found yet, requeueing"), true, false) } - orgsClient, err := logicalclusterclient.NewForLogicalCluster(w.mgr.GetLocalManager().GetConfig(), w.mgr.GetLocalManager().GetScheme(), logicalcluster.Name("root:orgs")) + orgsClient, err := iclient.NewForLogicalCluster(w.mgr.GetLocalManager().GetConfig(), w.mgr.GetLocalManager().GetScheme(), logicalcluster.Name("root:orgs")) if err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting parent organisation client: %w", err), true, true) } diff --git a/pkg/fga/tuples.go b/pkg/fga/tuples.go index 649c7d25..ece32a31 100644 --- a/pkg/fga/tuples.go +++ b/pkg/fga/tuples.go @@ -26,12 +26,12 @@ func TuplesForOrganization(acc accountv1alpha1.Account, ai accountv1alpha1.Accou func baseTuples(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInfo, creatorRelation, objectType string) []v1alpha1.Tuple { return []v1alpha1.Tuple{ - v1alpha1.Tuple{ + { User: fmt.Sprintf("user:%s", formatUser(*acc.Spec.Creator)), Relation: "assignee", Object: fmt.Sprintf("role:%s/%s/%s/owner", objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), }, - v1alpha1.Tuple{ + { User: fmt.Sprintf("role:%s/%s/%s/owner#assignee", objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), Relation: creatorRelation, Object: fmt.Sprintf("%s:%s/%s", objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), From ec189a4392bbbae2a42c2a54aa9279989aa559c3 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 10 Feb 2026 12:50:16 +0000 Subject: [PATCH 27/61] fix: config: correct FGA mapstructure paths On-behalf-of: @SAP --- internal/config/config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 31ca1078..a3724f90 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,9 +10,9 @@ type InviteConfig struct { type Config struct { FGA struct { Target string `mapstructure:"fga-target"` - ObjectType string `mapstructure:"subroutines-fga-object-type" default:"core_platform-mesh_io_account"` - ParentRelation string `mapstructure:"subroutines-fga-parent-relation" default:"parent"` - CreatorRelation string `mapstructure:"subroutines-fga-creator-relation" default:"owner"` + ObjectType string `mapstructure:"fga-object-type" default:"core_platform-mesh_io_account"` + ParentRelation string `mapstructure:"fga-parent-relation" default:"parent"` + CreatorRelation string `mapstructure:"fga-creator-relation" default:"owner"` } `mapstructure:",squash"` KCP struct { Kubeconfig string `mapstructure:"kcp-kubeconfig" default:"/api-kubeconfig/kubeconfig"` From cbfe46a8dca55cc65ead35049bc475264db0222d Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 10 Feb 2026 13:32:26 +0000 Subject: [PATCH 28/61] feat: org-initalization: check store readiness on tuple change On-behalf-of: @SAP --- internal/subroutine/workspace_initializer.go | 44 ++++++++++++-------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/internal/subroutine/workspace_initializer.go b/internal/subroutine/workspace_initializer.go index c7f1a5b2..8008b9bb 100644 --- a/internal/subroutine/workspace_initializer.go +++ b/internal/subroutine/workspace_initializer.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "slices" "strings" accountsv1alpha1 "github.com/platform-mesh/account-operator/api/v1alpha1" @@ -115,30 +116,39 @@ func (w *workspaceInitializer) Process(ctx context.Context, instance runtimeobje ObjectMeta: metav1.ObjectMeta{Name: generateStoreName(lc)}, } - if _, err := controllerutil.CreateOrUpdate(ctx, w.orgsClient, &store, func() error { + tuples := fga.TuplesForOrganization(acc, ai, w.creatorRelation, w.objectType) + if w.cfg.AllowMemberTuplesEnabled { // TODO: remove this flag once the feature is tested and stable + tuples = append(tuples, []v1alpha1.Tuple{ + { + Object: "role:authenticated", + Relation: "assignee", + User: "user:*", + }, + { + Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", lc.Spec.Owner.Cluster, lc.Spec.Owner.Name), + Relation: "member", + User: "role:authenticated#assignee", + }, + }...) + } + if result, err := controllerutil.CreateOrUpdate(ctx, w.orgsClient, &store, func() error { store.Spec = v1alpha1.StoreSpec{ CoreModule: w.coreModule, } - store.Spec.Tuples = fga.TuplesForOrganization(acc, ai, w.creatorRelation, w.objectType) - - if w.cfg.AllowMemberTuplesEnabled { // TODO: remove this flag once the feature is tested and stable - store.Spec.Tuples = []v1alpha1.Tuple{ - { - Object: "role:authenticated", - Relation: "assignee", - User: "user:*", - }, - { - Object: fmt.Sprintf("core_platform-mesh_io_account:%s/%s", lc.Spec.Owner.Cluster, lc.Spec.Owner.Name), - Relation: "member", - User: "role:authenticated#assignee", - }, - } - } + store.Spec.Tuples = tuples return nil }); err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("unable to create/update store: %w", err), true, true) + } else if result == controllerutil.OperationResultCreated || result == controllerutil.OperationResultUpdated { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("store needed to be updated, requeueing"), true, false) + } + + // Check if Store applied tuple changes + for _, t := range tuples { + if !slices.Contains(store.Status.ManagedTuples, t) { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("store does not yet contain all specified tuples, requeueing"), true, false) + } } if store.Status.StoreID == "" { From 8a35ddfacf684fcc8389b40b95ffb1aba6514ab9 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 11 Feb 2026 06:07:56 +0000 Subject: [PATCH 29/61] fix: account-initializer: get AccountInfo from parent Account's Workspace On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index f4be2cca..ad6744e5 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -64,23 +64,25 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("AccountInfo not found yet, requeueing"), true, false) } - parentOrgClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(ai.Spec.Organization.Path)) + // The actual Account resource belonging to the Workospace needs to be + // fetched from the parent Account's Workspace + parentAccountClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(ai.Spec.ParentAccount.Path)) if err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting parent organisation client: %w", err), true, true) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting parent account cluster client: %w", err), true, true) } - var acc accountsv1alpha1.Account - if err := parentOrgClient.Get(ctx, client.ObjectKey{ + if err := parentAccountClient.Get(ctx, client.ObjectKey{ Name: ai.Spec.Account.Name, }, &acc); err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Account in parent organisation: %w", err), true, true) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Account in parent account cluster: %w", err), true, true) } + // The Store to be updated belongs the parent Organization(which is not + // necessarily the parent Account!) but exists in the "orgs" Workspace orgsClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name("root:orgs")) if err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting orgs client: %w", err), true, true) } - var st v1alpha1.Store if err := orgsClient.Get(ctx, client.ObjectKey{ Name: ai.Spec.Organization.Name, From 4beb6b2d06d3110e2c58ce1fce1d3262fc1017f9 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 11 Feb 2026 06:44:21 +0000 Subject: [PATCH 30/61] chore: clarifying comments On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index ad6744e5..ace7e78e 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -50,11 +50,12 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo log = log.ChildLogger("ID", lcID).ChildLogger("path", p) log.Info().Msgf("Processing logical cluster") + // The AccountInfo in the logical custer belongs to the Account the + // Workspace was created for lcClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(lcID)) if err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting client: %w", err), true, true) } - var ai accountsv1alpha1.AccountInfo if err := lcClient.Get(ctx, client.ObjectKey{ Name: "account", @@ -64,7 +65,7 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("AccountInfo not found yet, requeueing"), true, false) } - // The actual Account resource belonging to the Workospace needs to be + // The actual Account resource belonging to the Workspace needs to be // fetched from the parent Account's Workspace parentAccountClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(ai.Spec.ParentAccount.Path)) if err != nil { From 1eaee22e8058db4fb58c82ce09c55d4b6f20d533 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 11 Feb 2026 12:38:00 +0000 Subject: [PATCH 31/61] fix: fga: check tuple building input for errors On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 5 ++++- internal/subroutine/workspace_initializer.go | 5 ++++- pkg/fga/tuples.go | 22 +++++++++++++------- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index ace7e78e..390e7fe5 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -93,7 +93,10 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo // Append the stores tuples with every tuple for the Account not yet managed // via the Store resource - tuples := fga.TuplesForAccount(acc, ai, s.creatorRelation, s.parentRelation, s.objectType) + tuples, err := fga.TuplesForAccount(acc, ai, s.creatorRelation, s.parentRelation, s.objectType) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("building tuples for account: %w", err), true, true) + } var changed bool for _, t := range tuples { if !slices.Contains(st.Spec.Tuples, t) { diff --git a/internal/subroutine/workspace_initializer.go b/internal/subroutine/workspace_initializer.go index 8008b9bb..15fcda12 100644 --- a/internal/subroutine/workspace_initializer.go +++ b/internal/subroutine/workspace_initializer.go @@ -116,7 +116,10 @@ func (w *workspaceInitializer) Process(ctx context.Context, instance runtimeobje ObjectMeta: metav1.ObjectMeta{Name: generateStoreName(lc)}, } - tuples := fga.TuplesForOrganization(acc, ai, w.creatorRelation, w.objectType) + tuples, err := fga.TuplesForOrganization(acc, ai, w.creatorRelation, w.objectType) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("building tuples for organization: %w", err), true, true) + } if w.cfg.AllowMemberTuplesEnabled { // TODO: remove this flag once the feature is tested and stable tuples = append(tuples, []v1alpha1.Tuple{ { diff --git a/pkg/fga/tuples.go b/pkg/fga/tuples.go index ece32a31..bb6d6943 100644 --- a/pkg/fga/tuples.go +++ b/pkg/fga/tuples.go @@ -1,6 +1,7 @@ package fga import ( + "errors" "fmt" "strings" @@ -9,22 +10,29 @@ import ( ) // TuplesForAccount returns FGA tuples for an account not of type organization. -func TuplesForAccount(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInfo, creatorRelation, parentRelation, objectType string) []v1alpha1.Tuple { - tuples := append(baseTuples(acc, ai, creatorRelation, objectType), v1alpha1.Tuple{ +func TuplesForAccount(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInfo, creatorRelation, parentRelation, objectType string) ([]v1alpha1.Tuple, error) { + base, err := baseTuples(acc, ai, creatorRelation, objectType) + if err != nil { + return nil, err + } + tuples := append(base, v1alpha1.Tuple{ User: fmt.Sprintf("%s:%s/%s", objectType, ai.Spec.ParentAccount.OriginClusterId, ai.Spec.ParentAccount.Name), Relation: parentRelation, Object: fmt.Sprintf("%s:%s/%s", objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), }) - - return tuples + return tuples, nil } // TuplesForOrganization returns FGA tuples for an Account of type organization. -func TuplesForOrganization(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInfo, creatorRelation, objectType string) []v1alpha1.Tuple { +func TuplesForOrganization(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInfo, creatorRelation, objectType string) ([]v1alpha1.Tuple, error) { return baseTuples(acc, ai, creatorRelation, objectType) } -func baseTuples(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInfo, creatorRelation, objectType string) []v1alpha1.Tuple { +func baseTuples(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInfo, creatorRelation, objectType string) ([]v1alpha1.Tuple, error) { + if acc.Spec.Creator == nil { + return nil, errors.New("account creator is nil") + } + return []v1alpha1.Tuple{ { User: fmt.Sprintf("user:%s", formatUser(*acc.Spec.Creator)), @@ -36,7 +44,7 @@ func baseTuples(acc accountv1alpha1.Account, ai accountv1alpha1.AccountInfo, cre Relation: creatorRelation, Object: fmt.Sprintf("%s:%s/%s", objectType, ai.Spec.Account.OriginClusterId, ai.Spec.Account.Name), }, - } + }, nil } // formatUser formats a user to be stored in an FGA tuple, i.e. replaces colons From 45f4b9c102fdc5ec9186c9c57fa7963d2d33c33b Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 11 Feb 2026 12:38:45 +0000 Subject: [PATCH 32/61] fix: remove now unused parentRelation from org initalizer On-behalf-of: @SAP --- internal/subroutine/workspace_initializer.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/subroutine/workspace_initializer.go b/internal/subroutine/workspace_initializer.go index 15fcda12..7c5ac384 100644 --- a/internal/subroutine/workspace_initializer.go +++ b/internal/subroutine/workspace_initializer.go @@ -30,7 +30,7 @@ import ( kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" ) -func NewWorkspaceInitializer(orgsClient client.Client, cfg config.Config, mgr mcmanager.Manager, creatorRelation, parentRelation, objectType string) *workspaceInitializer { +func NewWorkspaceInitializer(orgsClient client.Client, cfg config.Config, mgr mcmanager.Manager, creatorRelation, objectType string) *workspaceInitializer { // read file from path res, err := os.ReadFile(cfg.CoreModulePath) if err != nil { @@ -44,7 +44,6 @@ func NewWorkspaceInitializer(orgsClient client.Client, cfg config.Config, mgr mc mgr: mgr, cfg: cfg, creatorRelation: creatorRelation, - parentRelation: parentRelation, objectType: objectType, } } @@ -59,7 +58,6 @@ type workspaceInitializer struct { initializerName string objectType string - parentRelation string creatorRelation string } From 7a2d40896a9484c2fed163c64f7c2d9e7999bce0 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 11 Feb 2026 12:39:22 +0000 Subject: [PATCH 33/61] fix: client: check APIExportEndpointSlice length On-behalf-of: @SAP --- internal/client/all_platformmesh.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/client/all_platformmesh.go b/internal/client/all_platformmesh.go index 5b1b39de..af67d066 100644 --- a/internal/client/all_platformmesh.go +++ b/internal/client/all_platformmesh.go @@ -34,6 +34,10 @@ func NewForAllPlatformMeshResources(ctx context.Context, config *rest.Config, sc return nil, fmt.Errorf("getting %s APIExportEndpointSlice: %w", corePlatformMeshIOAPIExportEndpointSlice, err) } + if len(apiExportEndpointSlice.Status.APIExportEndpoints) == 0 { + return nil, fmt.Errorf("no endpoints found in %s APIExportEndpointSlice", corePlatformMeshIOAPIExportEndpointSlice) + } + virtualWorkspaceUrl, err := url.Parse(apiExportEndpointSlice.Status.APIExportEndpoints[0].URL) if err != nil { return nil, fmt.Errorf("parsing virtual workspace URL: %w", err) From 13af20e683bc912cef6328cb8ffd1f2636b45603 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 11 Feb 2026 12:39:57 +0000 Subject: [PATCH 34/61] fix: predicates: check slice length On-behalf-of: @SAP --- internal/predicates/accounttype.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/predicates/accounttype.go b/internal/predicates/accounttype.go index 7dd1b396..a162d91c 100644 --- a/internal/predicates/accounttype.go +++ b/internal/predicates/accounttype.go @@ -21,6 +21,6 @@ func LogicalClusterIsAccountTypeOrg() predicate.Predicate { parts := strings.Split(p, ":") - return parts[0] == "root" && parts[1] == "orgs" && len(parts) == 3 + return len(parts) == 3 && parts[0] == "root" && parts[1] == "orgs" }) } From f52e0ba7c1e49b43d97be1c34dbec279d17dc702 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 11 Feb 2026 12:41:03 +0000 Subject: [PATCH 35/61] fix: remove now unused parentRelation from org initalizer On-behalf-of: @SAP --- internal/controller/orglogicalcluster_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/orglogicalcluster_controller.go b/internal/controller/orglogicalcluster_controller.go index 26bdef11..45a6abe7 100644 --- a/internal/controller/orglogicalcluster_controller.go +++ b/internal/controller/orglogicalcluster_controller.go @@ -30,7 +30,7 @@ func NewOrgLogicalClusterReconciler(log *logger.Logger, orgClient client.Client, var subroutines []lifecyclesubroutine.Subroutine if cfg.Initializer.WorkspaceInitializerEnabled { - subroutines = append(subroutines, subroutine.NewWorkspaceInitializer(orgClient, cfg, mgr, cfg.FGA.CreatorRelation, cfg.FGA.ParentRelation, cfg.FGA.ObjectType)) + subroutines = append(subroutines, subroutine.NewWorkspaceInitializer(orgClient, cfg, mgr, cfg.FGA.CreatorRelation, cfg.FGA.ObjectType)) } if cfg.Initializer.IDPEnabled { subroutines = append(subroutines, subroutine.NewIDPSubroutine(orgClient, mgr, cfg)) From 9195391f022734cb12146e52ce4d78ab951e62b4 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 11 Feb 2026 12:42:14 +0000 Subject: [PATCH 36/61] fix: account-tuples: remove unused finalizer and check context cluster On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index 390e7fe5..15921acf 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -9,7 +9,6 @@ import ( "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/errors" - "github.com/platform-mesh/golang-commons/logger" "github.com/platform-mesh/security-operator/api/v1alpha1" iclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/pkg/fga" @@ -39,16 +38,15 @@ type AccountTuplesSubroutine struct { // Process implements lifecycle.Subroutine. func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { - log := logger.LoadLoggerFromContext(ctx) - lc := instance.(*kcpcorev1alpha1.LogicalCluster) p := lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] if p == "" { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster is not set"), true, true) } - lcID, _ := mccontext.ClusterFrom(ctx) - log = log.ChildLogger("ID", lcID).ChildLogger("path", p) - log.Info().Msgf("Processing logical cluster") + lcID, ok := mccontext.ClusterFrom(ctx) + if !ok { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("cluster name not found in context"), true, true) + } // The AccountInfo in the logical custer belongs to the Account the // Workspace was created for @@ -133,21 +131,12 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo // Finalize implements lifecycle.Subroutine. func (s *AccountTuplesSubroutine) Finalize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { - log := logger.LoadLoggerFromContext(ctx) - - lc := instance.(*kcpcorev1alpha1.LogicalCluster) - p := lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] - if p == "" { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster is not set"), true, true) - } - log.Info().Msgf("Finalizing logical cluster of path %s", p) - return ctrl.Result{}, nil } // Finalizers implements lifecycle.Subroutine. func (s *AccountTuplesSubroutine) Finalizers(_ runtimeobject.RuntimeObject) []string { - return []string{"core.platform-mesh.io/account-fga-tuples"} + return []string{} } // GetName implements lifecycle.Subroutine. From a47af51c2d1917a4b7662d35e831e651a571297b Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 11 Feb 2026 12:45:08 +0000 Subject: [PATCH 37/61] chore: fix typo in comment On-behalf-of: @SAP --- internal/controller/accountlogicalcluster_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/accountlogicalcluster_controller.go b/internal/controller/accountlogicalcluster_controller.go index 172beec0..301ee75e 100644 --- a/internal/controller/accountlogicalcluster_controller.go +++ b/internal/controller/accountlogicalcluster_controller.go @@ -20,7 +20,7 @@ import ( kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" ) -// AccountLogicalClusterReconciler acts as an intializer for account workspaces. +// AccountLogicalClusterReconciler acts as an initializer for account workspaces. type AccountLogicalClusterReconciler struct { log *logger.Logger From ad0fc3d0f5dfb751740b5244e2bd6374cbaaaa48 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 11 Feb 2026 12:45:30 +0000 Subject: [PATCH 38/61] fix: typo in error message On-behalf-of: @SAP --- internal/subroutine/invite/subroutine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/subroutine/invite/subroutine.go b/internal/subroutine/invite/subroutine.go index 97a2acd1..543143f3 100644 --- a/internal/subroutine/invite/subroutine.go +++ b/internal/subroutine/invite/subroutine.go @@ -219,7 +219,7 @@ func (s *subroutine) Process(ctx context.Context, instance runtimeobject.Runtime defer res.Body.Close() //nolint:errcheck if res.StatusCode != http.StatusCreated { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("keylcloak returned non-200 status code: %s", res.Status), true, true) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("keycloak returned non-201 status code: %s", res.Status), true, true) } res, err = s.keycloak.Get(fmt.Sprintf("%s/admin/realms/%s/users?%s", s.keycloakBaseURL, realm, v.Encode())) From e405a78919f90efa27c306057e76a78c5fa8350a Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 11 Feb 2026 12:46:36 +0000 Subject: [PATCH 39/61] fix: org-initalizer: remove pointless logger On-behalf-of: @SAP --- internal/subroutine/workspace_initializer.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/subroutine/workspace_initializer.go b/internal/subroutine/workspace_initializer.go index 7c5ac384..b1c27e8e 100644 --- a/internal/subroutine/workspace_initializer.go +++ b/internal/subroutine/workspace_initializer.go @@ -11,7 +11,6 @@ import ( "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/errors" - "github.com/platform-mesh/golang-commons/logger" "github.com/platform-mesh/security-operator/api/v1alpha1" iclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/internal/config" @@ -73,16 +72,12 @@ func (w *workspaceInitializer) Finalizers(_ runtimeobject.RuntimeObject) []strin func (w *workspaceInitializer) GetName() string { return "WorkspaceInitializer" } func (w *workspaceInitializer) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { - log := logger.LoadLoggerFromContext(ctx) - lc := instance.(*kcpcorev1alpha1.LogicalCluster) p := lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] if p == "" { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster is not set"), true, true) } lcID, _ := mccontext.ClusterFrom(ctx) - log = log.ChildLogger("ID", lcID).ChildLogger("path", p) - log.Info().Msgf("Processing logical cluster") lcClient, err := iclient.NewForLogicalCluster(w.mgr.GetLocalManager().GetConfig(), w.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(lcID)) if err != nil { From 37709cd248c924519c956732729530f08735c1ed Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 11 Feb 2026 12:58:05 +0000 Subject: [PATCH 40/61] fix: initializer: use correct config On-behalf-of: @SAP --- cmd/initializer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/initializer.go b/cmd/initializer.go index a1fefe4b..9cdc37c6 100644 --- a/cmd/initializer.go +++ b/cmd/initializer.go @@ -103,7 +103,7 @@ var initializerCmd = &cobra.Command{ os.Exit(1) } - kcpCfg, err := getKubeconfigFromPath(operatorCfg.KCP.Kubeconfig) + kcpCfg, err := getKubeconfigFromPath(initializerCfg.KCP.Kubeconfig) if err != nil { log.Error().Err(err).Msg("unable to get KCP kubeconfig") return err From 3483a52e1884278c285d6a3c40cd07c8ea3eea3b Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Thu, 12 Feb 2026 06:17:16 +0000 Subject: [PATCH 41/61] feat: add tuple manager On-behalf-of: @SAP --- internal/subroutine/tuples.go | 84 +++----------- internal/subroutine/tuples_test.go | 18 +-- pkg/fga/tuple_manager.go | 90 +++++++++++++++ pkg/fga/tuple_manager_test.go | 177 +++++++++++++++++++++++++++++ 4 files changed, 288 insertions(+), 81 deletions(-) create mode 100644 pkg/fga/tuple_manager.go create mode 100644 pkg/fga/tuple_manager_test.go diff --git a/internal/subroutine/tuples.go b/internal/subroutine/tuples.go index c6dfc2a3..4b18b6d1 100644 --- a/internal/subroutine/tuples.go +++ b/internal/subroutine/tuples.go @@ -9,9 +9,9 @@ import ( "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/errors" - "github.com/platform-mesh/golang-commons/fga/helpers" "github.com/platform-mesh/golang-commons/logger" securityv1alpha1 "github.com/platform-mesh/security-operator/api/v1alpha1" + "github.com/platform-mesh/security-operator/pkg/fga" ctrl "sigs.k8s.io/controller-runtime" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" @@ -56,27 +56,9 @@ func (t *tupleSubroutine) Finalize(ctx context.Context, instance runtimeobject.R authorizationModelID = store.Status.AuthorizationModelID } - for _, tuple := range managedTuples { - _, err := t.fga.Write(ctx, &openfgav1.WriteRequest{ - StoreId: storeID, - AuthorizationModelId: authorizationModelID, - Deletes: &openfgav1.WriteRequestDeletes{ - TupleKeys: []*openfgav1.TupleKeyWithoutCondition{ - { - Object: tuple.Object, - Relation: tuple.Relation, - User: tuple.User, - }, - }, - }, - }) - if helpers.IsDuplicateWriteError(err) { // coverage-ignore - log.Info().Stringer("tuple", tuple).Msg("tuple already deleted") - continue - } - if err != nil { - return ctrl.Result{}, errors.NewOperatorError(err, false, true) - } + tm := fga.NewTupleManager(t.fga, storeID, authorizationModelID, log) + if err := tm.Delete(ctx, managedTuples); err != nil { + return ctrl.Result{}, errors.NewOperatorError(err, false, true) } switch obj := instance.(type) { @@ -134,57 +116,21 @@ func (t *tupleSubroutine) Process(ctx context.Context, instance runtimeobject.Ru authorizationModelID = store.Status.AuthorizationModelID } - for _, tuple := range specTuples { - _, err := t.fga.Write(ctx, &openfgav1.WriteRequest{ - StoreId: storeID, - AuthorizationModelId: authorizationModelID, - Writes: &openfgav1.WriteRequestWrites{ - TupleKeys: []*openfgav1.TupleKey{ - { - Object: tuple.Object, - Relation: tuple.Relation, - User: tuple.User, - }, - }, - }, - }) - if helpers.IsDuplicateWriteError(err) { // coverage-ignore - log.Info().Stringer("tuple", tuple).Msg("tuple already exists") - continue - } - if err != nil { - return ctrl.Result{}, errors.NewOperatorError(err, false, true) - } + tm := fga.NewTupleManager(t.fga, storeID, authorizationModelID, log) + if err := tm.Apply(ctx, specTuples); err != nil { + return ctrl.Result{}, errors.NewOperatorError(err, false, true) } + var tuplesToDelete []securityv1alpha1.Tuple for _, tuple := range managedTuples { - if idx := slices.IndexFunc(specTuples, func(t securityv1alpha1.Tuple) bool { - return t.Object == tuple.Object && t.Relation == tuple.Relation && t.User == tuple.User - }); idx != -1 { - continue + if slices.IndexFunc(specTuples, func(s securityv1alpha1.Tuple) bool { + return s.Object == tuple.Object && s.Relation == tuple.Relation && s.User == tuple.User + }) == -1 { + tuplesToDelete = append(tuplesToDelete, tuple) } - - _, err := t.fga.Write(ctx, &openfgav1.WriteRequest{ - StoreId: storeID, - AuthorizationModelId: authorizationModelID, - Deletes: &openfgav1.WriteRequestDeletes{ - TupleKeys: []*openfgav1.TupleKeyWithoutCondition{ - { - Object: tuple.Object, - Relation: tuple.Relation, - User: tuple.User, - }, - }, - }, - }) - if helpers.IsDuplicateWriteError(err) { // coverage-ignore - log.Info().Stringer("tuple", tuple).Msg("tuple already deleted") - continue - } - if err != nil { // coverage-ignore - return ctrl.Result{}, errors.NewOperatorError(err, false, true) - } - + } + if err := tm.Delete(ctx, tuplesToDelete); err != nil { + return ctrl.Result{}, errors.NewOperatorError(err, false, true) } switch obj := instance.(type) { diff --git a/internal/subroutine/tuples_test.go b/internal/subroutine/tuples_test.go index fe53d42c..7301da5f 100644 --- a/internal/subroutine/tuples_test.go +++ b/internal/subroutine/tuples_test.go @@ -70,7 +70,7 @@ func TestTupleProcessWithStore(t *testing.T) { }, }, fgaMocks: func(fga *mocks.MockOpenFGAServiceClient) { - fga.EXPECT().Write(mock.Anything, mock.Anything).Return(nil, nil).Times(3) + fga.EXPECT().Write(mock.Anything, mock.Anything).Return(nil, nil) }, }, { @@ -108,11 +108,8 @@ func TestTupleProcessWithStore(t *testing.T) { }, }, fgaMocks: func(fga *mocks.MockOpenFGAServiceClient) { - // write calls - fga.EXPECT().Write(mock.Anything, mock.Anything).Return(nil, nil).Times(3) - - // delete call - fga.EXPECT().Write(mock.Anything, mock.Anything).Return(nil, nil) + // Apply (batch write) + Delete (batch delete) + fga.EXPECT().Write(mock.Anything, mock.Anything).Return(nil, nil).Twice() }, }, { @@ -223,7 +220,7 @@ func TestTupleProcessWithAuthorizationModel(t *testing.T) { }, }, fgaMocks: func(fga *mocks.MockOpenFGAServiceClient) { - fga.EXPECT().Write(mock.Anything, mock.Anything).Return(nil, nil).Times(3) + fga.EXPECT().Write(mock.Anything, mock.Anything).Return(nil, nil) }, k8sMocks: func(k8s *mocks.MockClient) { // Not used for AuthorizationModel @@ -287,11 +284,8 @@ func TestTupleProcessWithAuthorizationModel(t *testing.T) { }, }, fgaMocks: func(fga *mocks.MockOpenFGAServiceClient) { - // write calls - fga.EXPECT().Write(mock.Anything, mock.Anything).Return(nil, nil).Times(3) - - // delete call - fga.EXPECT().Write(mock.Anything, mock.Anything).Return(nil, nil) + // 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 diff --git a/pkg/fga/tuple_manager.go b/pkg/fga/tuple_manager.go new file mode 100644 index 00000000..0cc4b846 --- /dev/null +++ b/pkg/fga/tuple_manager.go @@ -0,0 +1,90 @@ +package fga + +import ( + "context" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/platform-mesh/golang-commons/logger" + "github.com/platform-mesh/security-operator/api/v1alpha1" +) + +// TupleManager wraps around FGA attributes to write and delete sets of tuples. +type TupleManager struct { + client openfgav1.OpenFGAServiceClient + storeID string + authorizationModelID string + logger logger.Logger +} + +func NewTupleManager(client openfgav1.OpenFGAServiceClient, storeID, authorizationModelID string, log *logger.Logger) *TupleManager { + return &TupleManager{ + client: client, + storeID: storeID, + authorizationModelID: authorizationModelID, + logger: *log.ComponentLogger("tuple_manager").MustChildLoggerWithAttributes("store_id", storeID, "authorization_model", authorizationModelID), + } +} + +// Apply writes a given set of tuples within a single transaction and ignores +// duplicate writes. +func (m *TupleManager) Apply(ctx context.Context, tuples []v1alpha1.Tuple) error { + if len(tuples) == 0 { + return nil + } + + tupleKeys := make([]*openfgav1.TupleKey, 0, len(tuples)) + for _, t := range tuples { + tupleKeys = append(tupleKeys, &openfgav1.TupleKey{ + Object: t.Object, + Relation: t.Relation, + User: t.User, + }) + } + + _, err := m.client.Write(ctx, &openfgav1.WriteRequest{ + StoreId: m.storeID, + AuthorizationModelId: m.authorizationModelID, + Writes: &openfgav1.WriteRequestWrites{ + TupleKeys: tupleKeys, + OnDuplicate: "ignore", + }, + }) + if err != nil { + return err + } + + m.logger.Debug().Int("count", len(tuples)).Msg("Wrote tuples") + return nil +} + +// Delete deletes a given set of tuples within a single transaction and ignores +// duplicate deletions. +func (m *TupleManager) Delete(ctx context.Context, tuples []v1alpha1.Tuple) error { + if len(tuples) == 0 { + return nil + } + + tupleKeys := make([]*openfgav1.TupleKeyWithoutCondition, 0, len(tuples)) + for _, t := range tuples { + tupleKeys = append(tupleKeys, &openfgav1.TupleKeyWithoutCondition{ + Object: t.Object, + Relation: t.Relation, + User: t.User, + }) + } + + _, err := m.client.Write(ctx, &openfgav1.WriteRequest{ + StoreId: m.storeID, + AuthorizationModelId: m.authorizationModelID, + Deletes: &openfgav1.WriteRequestDeletes{ + TupleKeys: tupleKeys, + OnMissing: "ignore", + }, + }) + if err != nil { + return err + } + + m.logger.Debug().Int("count", len(tuples)).Msg("Deleted tuples") + return nil +} diff --git a/pkg/fga/tuple_manager_test.go b/pkg/fga/tuple_manager_test.go new file mode 100644 index 00000000..559696d7 --- /dev/null +++ b/pkg/fga/tuple_manager_test.go @@ -0,0 +1,177 @@ +package fga + +import ( + "context" + "errors" + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/platform-mesh/golang-commons/logger/testlogger" + "github.com/platform-mesh/security-operator/api/v1alpha1" + "github.com/platform-mesh/security-operator/internal/subroutine/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +func TestTupleManager_Apply(t *testing.T) { + t.Run("returns nil for empty tuples", func(t *testing.T) { + client := mocks.NewMockOpenFGAServiceClient(t) + log := testlogger.New() + mgr := NewTupleManager(client, "store-id", "model-id", log.Logger) + + err := mgr.Apply(context.Background(), nil) + assert.NoError(t, err) + + err = mgr.Apply(context.Background(), []v1alpha1.Tuple{}) + assert.NoError(t, err) + }) + + t.Run("writes tuples successfully", func(t *testing.T) { + client := mocks.NewMockOpenFGAServiceClient(t) + client.EXPECT().Write(mock.Anything, mock.MatchedBy(func(req *openfgav1.WriteRequest) bool { + return req.StoreId == "store-id" && + req.AuthorizationModelId == "model-id" && + req.Writes != nil && + len(req.Writes.TupleKeys) == 2 && + req.Writes.OnDuplicate == "ignore" + })).Return(&openfgav1.WriteResponse{}, nil) + + log := testlogger.New() + mgr := NewTupleManager(client, "store-id", "model-id", log.Logger) + + tuples := []v1alpha1.Tuple{ + {Object: "doc:1", Relation: "viewer", User: "user:alice"}, + {Object: "doc:2", Relation: "owner", User: "user:bob"}, + } + + err := mgr.Apply(context.Background(), tuples) + assert.NoError(t, err) + }) + + t.Run("returns error when write fails", func(t *testing.T) { + client := mocks.NewMockOpenFGAServiceClient(t) + client.EXPECT().Write(mock.Anything, mock.Anything).Return(nil, errors.New("write failed")) + + log := testlogger.New() + mgr := NewTupleManager(client, "store-id", "model-id", log.Logger) + + tuples := []v1alpha1.Tuple{ + {Object: "doc:1", Relation: "viewer", User: "user:alice"}, + } + + err := mgr.Apply(context.Background(), tuples) + assert.Error(t, err) + }) +} + +func TestTupleManager_Delete(t *testing.T) { + t.Run("returns nil for empty tuples", func(t *testing.T) { + client := mocks.NewMockOpenFGAServiceClient(t) + log := testlogger.New() + mgr := NewTupleManager(client, "store-id", "model-id", log.Logger) + + err := mgr.Delete(context.Background(), nil) + assert.NoError(t, err) + + err = mgr.Delete(context.Background(), []v1alpha1.Tuple{}) + assert.NoError(t, err) + }) + + t.Run("deletes tuples successfully", func(t *testing.T) { + client := mocks.NewMockOpenFGAServiceClient(t) + client.EXPECT().Write(mock.Anything, mock.MatchedBy(func(req *openfgav1.WriteRequest) bool { + return req.StoreId == "store-id" && + req.AuthorizationModelId == "model-id" && + req.Deletes != nil && + len(req.Deletes.TupleKeys) == 2 && + req.Deletes.OnMissing == "ignore" + })).Return(&openfgav1.WriteResponse{}, nil) + + log := testlogger.New() + mgr := NewTupleManager(client, "store-id", "model-id", log.Logger) + + tuples := []v1alpha1.Tuple{ + {Object: "doc:1", Relation: "viewer", User: "user:alice"}, + {Object: "doc:2", Relation: "owner", User: "user:bob"}, + } + + err := mgr.Delete(context.Background(), tuples) + assert.NoError(t, err) + }) + + t.Run("returns error when delete fails", func(t *testing.T) { + client := mocks.NewMockOpenFGAServiceClient(t) + client.EXPECT().Write(mock.Anything, mock.Anything).Return(nil, errors.New("delete failed")) + + log := testlogger.New() + mgr := NewTupleManager(client, "store-id", "model-id", log.Logger) + + tuples := []v1alpha1.Tuple{ + {Object: "doc:1", Relation: "viewer", User: "user:alice"}, + } + + err := mgr.Delete(context.Background(), tuples) + assert.Error(t, err) + }) +} + +func TestTupleManager_Apply_verifies_tuple_contents(t *testing.T) { + var capturedReq *openfgav1.WriteRequest + client := mocks.NewMockOpenFGAServiceClient(t) + client.EXPECT().Write(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, req *openfgav1.WriteRequest, opts ...grpc.CallOption) (*openfgav1.WriteResponse, error) { + capturedReq = req + return &openfgav1.WriteResponse{}, nil + }) + + log := testlogger.New() + mgr := NewTupleManager(client, "store-id", "model-id", log.Logger) + + tuples := []v1alpha1.Tuple{ + {Object: "doc:1", Relation: "viewer", User: "user:alice"}, + {Object: "doc:2", Relation: "owner", User: "user:bob"}, + } + + err := mgr.Apply(context.Background(), tuples) + require.NoError(t, err) + require.NotNil(t, capturedReq) + require.NotNil(t, capturedReq.Writes) + require.Len(t, capturedReq.Writes.TupleKeys, 2) + + // Verify both tuples are in the request + keys := capturedReq.Writes.TupleKeys + assert.True(t, (keys[0].Object == "doc:1" && keys[0].Relation == "viewer" && keys[0].User == "user:alice") || + (keys[1].Object == "doc:1" && keys[1].Relation == "viewer" && keys[1].User == "user:alice")) + assert.True(t, (keys[0].Object == "doc:2" && keys[0].Relation == "owner" && keys[0].User == "user:bob") || + (keys[1].Object == "doc:2" && keys[1].Relation == "owner" && keys[1].User == "user:bob")) +} + +func TestTupleManager_Delete_verifies_tuple_contents(t *testing.T) { + var capturedReq *openfgav1.WriteRequest + client := mocks.NewMockOpenFGAServiceClient(t) + client.EXPECT().Write(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, req *openfgav1.WriteRequest, opts ...grpc.CallOption) (*openfgav1.WriteResponse, error) { + capturedReq = req + return &openfgav1.WriteResponse{}, nil + }) + + log := testlogger.New() + mgr := NewTupleManager(client, "store-id", "model-id", log.Logger) + + tuples := []v1alpha1.Tuple{ + {Object: "doc:1", Relation: "viewer", User: "user:alice"}, + {Object: "doc:2", Relation: "owner", User: "user:bob"}, + } + + err := mgr.Delete(context.Background(), tuples) + require.NoError(t, err) + require.NotNil(t, capturedReq) + require.NotNil(t, capturedReq.Deletes) + require.Len(t, capturedReq.Deletes.TupleKeys, 2) + + keys := capturedReq.Deletes.TupleKeys + assert.True(t, (keys[0].Object == "doc:1" && keys[0].Relation == "viewer" && keys[0].User == "user:alice") || + (keys[1].Object == "doc:1" && keys[1].Relation == "viewer" && keys[1].User == "user:alice")) + assert.True(t, (keys[0].Object == "doc:2" && keys[0].Relation == "owner" && keys[0].User == "user:bob") || + (keys[1].Object == "doc:2" && keys[1].Relation == "owner" && keys[1].User == "user:bob")) +} From 189ca8a1b303c67d4082d00bb12cd627666ff2b2 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Thu, 12 Feb 2026 07:47:40 +0000 Subject: [PATCH 42/61] feat: tuple manager: add latest store constant On-behalf-of: @SAP --- pkg/fga/tuple_manager.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/fga/tuple_manager.go b/pkg/fga/tuple_manager.go index 0cc4b846..3e477522 100644 --- a/pkg/fga/tuple_manager.go +++ b/pkg/fga/tuple_manager.go @@ -8,6 +8,10 @@ import ( "github.com/platform-mesh/security-operator/api/v1alpha1" ) +// AuthorizationModelIDLatest is to explicitely acknowledge that no ID means +// latest. +const AuthorizationModelIDLatest = "" + // TupleManager wraps around FGA attributes to write and delete sets of tuples. type TupleManager struct { client openfgav1.OpenFGAServiceClient From d9c17f77c02191aced0e2b233fbb512721337e6f Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Thu, 12 Feb 2026 07:48:31 +0000 Subject: [PATCH 43/61] feat: account-initializer: directly speak to openfga On-behalf-of: @SAP --- cmd/initializer.go | 12 ++++- .../accountlogicalcluster_controller.go | 5 +- internal/subroutine/account_tuples.go | 52 +++---------------- 3 files changed, 21 insertions(+), 48 deletions(-) diff --git a/cmd/initializer.go b/cmd/initializer.go index 9cdc37c6..d4802899 100644 --- a/cmd/initializer.go +++ b/cmd/initializer.go @@ -6,9 +6,12 @@ import ( helmv2 "github.com/fluxcd/helm-controller/api/v2" sourcev1 "github.com/fluxcd/source-controller/api/v1" + openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/platform-mesh/security-operator/internal/controller" "github.com/platform-mesh/security-operator/internal/predicates" "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" @@ -109,12 +112,19 @@ var initializerCmd = &cobra.Command{ return err } + conn, err := grpc.NewClient(initializerCfg.FGA.Target, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Error().Err(err).Msg("unable to create grpc client") + return err + } + fga := openfgav1.NewOpenFGAServiceClient(conn) + mcc, err := mcclient.New(kcpCfg, client.Options{Scheme: scheme}) if err != nil { log.Error().Err(err).Msg("Failed to create multicluster client") os.Exit(1) } - if err := controller.NewAccountLogicalClusterReconciler(log, initializerCfg, mcc, mgr). + if err := controller.NewAccountLogicalClusterReconciler(log, initializerCfg, fga, mcc, mgr). SetupWithManager(mgr, defaultCfg, predicate.Not(predicates.LogicalClusterIsAccountTypeOrg())); err != nil { setupLog.Error(err, "unable to create controller", "controller", "AccountLogicalCluster") os.Exit(1) diff --git a/internal/controller/accountlogicalcluster_controller.go b/internal/controller/accountlogicalcluster_controller.go index 301ee75e..a6b49e3f 100644 --- a/internal/controller/accountlogicalcluster_controller.go +++ b/internal/controller/accountlogicalcluster_controller.go @@ -3,6 +3,7 @@ package controller import ( "context" + openfgav1 "github.com/openfga/api/proto/openfga/v1" platformeshconfig "github.com/platform-mesh/golang-commons/config" "github.com/platform-mesh/golang-commons/controller/lifecycle/builder" "github.com/platform-mesh/golang-commons/controller/lifecycle/multicluster" @@ -27,11 +28,11 @@ type AccountLogicalClusterReconciler struct { mclifecycle *multicluster.LifecycleManager } -func NewAccountLogicalClusterReconciler(log *logger.Logger, cfg config.Config, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountLogicalClusterReconciler { +func NewAccountLogicalClusterReconciler(log *logger.Logger, cfg config.Config, fga openfgav1.OpenFGAServiceClient, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountLogicalClusterReconciler { return &AccountLogicalClusterReconciler{ log: log, mclifecycle: builder.NewBuilder("security", "AccountLogicalClusterReconciler", []lifecyclesubroutine.Subroutine{ - subroutine.NewAccountTuplesSubroutine(mcc, mgr, cfg.FGA.CreatorRelation, cfg.FGA.ParentRelation, cfg.FGA.ObjectType), + subroutine.NewAccountTuplesSubroutine(mcc, mgr, fga, cfg.FGA.CreatorRelation, cfg.FGA.ParentRelation, cfg.FGA.ObjectType), subroutine.NewRemoveInitializer(mgr, cfg), }, log). WithReadOnly(). diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index 15921acf..b41f8172 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -3,13 +3,13 @@ package subroutine import ( "context" "fmt" - "slices" + openfgav1 "github.com/openfga/api/proto/openfga/v1" accountsv1alpha1 "github.com/platform-mesh/account-operator/api/v1alpha1" "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/errors" - "github.com/platform-mesh/security-operator/api/v1alpha1" + "github.com/platform-mesh/golang-commons/logger" iclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/pkg/fga" ctrl "sigs.k8s.io/controller-runtime" @@ -30,6 +30,7 @@ import ( type AccountTuplesSubroutine struct { mgr mcmanager.Manager mcc mcclient.ClusterClient + fga openfgav1.OpenFGAServiceClient objectType string parentRelation string @@ -76,56 +77,16 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Account in parent account cluster: %w", err), true, true) } - // The Store to be updated belongs the parent Organization(which is not - // necessarily the parent Account!) but exists in the "orgs" Workspace - orgsClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name("root:orgs")) - if err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting orgs client: %w", err), true, true) - } - var st v1alpha1.Store - if err := orgsClient.Get(ctx, client.ObjectKey{ - Name: ai.Spec.Organization.Name, - }, &st); err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting parent organisation's Store: %w", err), true, true) - } - // Append the stores tuples with every tuple for the Account not yet managed // via the Store resource tuples, err := fga.TuplesForAccount(acc, ai, s.creatorRelation, s.parentRelation, s.objectType) if err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("building tuples for account: %w", err), true, true) } - var changed bool - for _, t := range tuples { - if !slices.Contains(st.Spec.Tuples, t) { - st.Spec.Tuples = append(st.Spec.Tuples, t) - changed = true - } - } - - // Potentially update Store and requeue to wait for update - // todo(simontesar): we could be watching Stores instead - if changed { - if err := orgsClient.Update(ctx, &st); err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("updating Store with tuples: %w", err), true, true) - } - - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("store needed to be updated, requeueing"), true, false) + if err := fga.NewTupleManager(s.fga, ai.Spec.FGA.Store.Id, fga.AuthorizationModelIDLatest, logger.LoadLoggerFromContext(ctx)).Apply(ctx, tuples); err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("applying tuples for Account: %w", err), true, true) } - // Check if Store applied tuple changes - for _, t := range tuples { - if !slices.Contains(st.Status.ManagedTuples, t) { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("store does not yet contain all specified tuples, requeueing"), true, false) - } - } - - // todo(simontesar): checking and waiting for Readiness is currently futile, - // our conditions don't include the observed generation - // if conditions.IsPresentAndEqualForGeneration(st.Status.Conditions, lcconditions.ConditionReady, metav1.ConditionTrue, st.GetObjectMeta().GetGeneration()) { - // return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("store %s is not ready", st.Name), true, false) - // } - return ctrl.Result{}, nil } @@ -142,10 +103,11 @@ func (s *AccountTuplesSubroutine) Finalizers(_ runtimeobject.RuntimeObject) []st // GetName implements lifecycle.Subroutine. func (s *AccountTuplesSubroutine) GetName() string { return "AccountTuplesSubroutine" } -func NewAccountTuplesSubroutine(mcc mcclient.ClusterClient, mgr mcmanager.Manager, creatorRelation, parentRelation, objectType string) *AccountTuplesSubroutine { +func NewAccountTuplesSubroutine(mcc mcclient.ClusterClient, mgr mcmanager.Manager, fga openfgav1.OpenFGAServiceClient, creatorRelation, parentRelation, objectType string) *AccountTuplesSubroutine { return &AccountTuplesSubroutine{ mgr: mgr, mcc: mcc, + fga: fga, creatorRelation: creatorRelation, parentRelation: parentRelation, objectType: objectType, From ec8e6e48d05292f007ff4e5ca244e87738736c0a Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Fri, 13 Feb 2026 09:47:04 +0000 Subject: [PATCH 44/61] feat: add terminatingworkspaces provider On-behalf-of: @SAP --- internal/terminatingworkspaces/provider.go | 105 +++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 internal/terminatingworkspaces/provider.go diff --git a/internal/terminatingworkspaces/provider.go b/internal/terminatingworkspaces/provider.go new file mode 100644 index 00000000..cb39b6be --- /dev/null +++ b/internal/terminatingworkspaces/provider.go @@ -0,0 +1,105 @@ +package terminatingworkspaces + +import ( + "github.com/go-logr/logr" + + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/cluster" + "sigs.k8s.io/controller-runtime/pkg/log" + + "sigs.k8s.io/multicluster-runtime/pkg/clusters" + "sigs.k8s.io/multicluster-runtime/pkg/multicluster" + + "github.com/kcp-dev/logicalcluster/v3" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" + kcptenancyv1alpha1 "github.com/kcp-dev/sdk/apis/tenancy/v1alpha1" + + mcpcache "github.com/kcp-dev/multicluster-provider/pkg/cache" + "github.com/kcp-dev/multicluster-provider/pkg/events/recorder" + "github.com/kcp-dev/multicluster-provider/pkg/provider" +) + +var _ multicluster.Provider = &Provider{} +var _ multicluster.ProviderRunnable = &Provider{} + +// Provider reconciles LogicalClusters that are in deletion and have a specific +// terminator. +// It is a slightly modified version of +// github.com/kcp-dev/multicluster-provider/initializingworkspaces. +type Provider struct { + provider.Factory +} + +// Options are the options for creating a new instance of the terminating workspaces provider. +type Options struct { + // Scheme is the scheme to use for the provider. If this is nil, it defaults + // to the client-go scheme. + Scheme *runtime.Scheme + + // Log is the logger to use for the provider. If this is nil, it defaults + // to the controller-runtime default logger. + Log *logr.Logger +} + +// New creates a new kcp terminating workspaces provider. +func New(cfg *rest.Config, workspaceTypeName string, options Options) (*Provider, error) { + // Do the defaulting controller-runtime would do for those fields we need. + if options.Scheme == nil { + options.Scheme = scheme.Scheme + } + + if options.Log == nil { + options.Log = ptr.To(log.Log.WithName("kcp-terminatingworkspaces-cluster-provider")) + } + + c, err := cache.New(cfg, cache.Options{ + Scheme: options.Scheme, + ByObject: map[client.Object]cache.ByObject{ + &kcptenancyv1alpha1.WorkspaceType{}: { + Field: fields.SelectorFromSet(fields.Set{"metadata.name": workspaceTypeName}), + }, + }, + }) + if err != nil { + return nil, err + } + + return &Provider{ + Factory: provider.Factory{ + Clusters: ptr.To(clusters.New[cluster.Cluster]()), + Providers: map[string]*provider.Provider{}, + + Log: *options.Log, + + GetVWs: func(obj client.Object) ([]string, error) { + wst := obj.(*kcptenancyv1alpha1.WorkspaceType) + var urls []string + for _, endpoint := range wst.Status.VirtualWorkspaces { + if endpoint.Type != "terminating" { + continue + } + urls = append(urls, endpoint.URL) + } + return urls, nil + }, + + Config: cfg, + Scheme: options.Scheme, + Outer: &kcptenancyv1alpha1.WorkspaceType{}, + Inner: &kcpcorev1alpha1.LogicalCluster{}, + Cache: c, + // ensure the generic provider builds a per-cluster cache instead of a wildcard-based + // cache, since this virtual workspace does not offer anything but logicalclusters on + // the wildcard endpoint + NewCluster: func(cfg *rest.Config, clusterName logicalcluster.Name, wildcardCA mcpcache.WildcardCache, scheme *runtime.Scheme, _ recorder.EventRecorderGetter) (*mcpcache.ScopedCluster, error) { + return mcpcache.NewScopedInitializingCluster(cfg, clusterName, wildcardCA, scheme) + }, + }, + }, nil +} From 5270bffc290941945f9cb04e677a28b5eabb2ab8 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 17 Feb 2026 09:40:30 +0000 Subject: [PATCH 45/61] fix: pin k8s dependencies to 0.34.0 On-behalf-of: @SAP --- go.mod | 27 ++++++++++++++------ go.sum | 77 ++++++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/go.mod b/go.mod index 7daca6d4..a725c099 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,13 @@ module github.com/platform-mesh/security-operator -go 1.25.0 +go 1.25.7 + +replace ( + k8s.io/api => k8s.io/api v0.34.0 + k8s.io/apiserver => k8s.io/apiserver v0.34.0 + k8s.io/client-go => k8s.io/client-go v0.34.0 + k8s.io/component-base => k8s.io/component-base v0.34.0 +) require ( github.com/coreos/go-oidc v2.5.0+incompatible @@ -9,7 +16,7 @@ require ( github.com/go-logr/logr v1.4.3 github.com/google/gnostic-models v0.7.1 github.com/kcp-dev/logicalcluster/v3 v3.0.5 - github.com/kcp-dev/multicluster-provider v0.4.0 + github.com/kcp-dev/multicluster-provider v0.5.0 github.com/kcp-dev/sdk v0.30.0 github.com/openfga/api/proto v0.0.0-20260122181957-618e7e0a4878 github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c @@ -17,18 +24,20 @@ require ( github.com/platform-mesh/golang-commons v0.9.36 github.com/rs/zerolog v1.34.0 github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 + go.uber.org/zap v1.27.1 golang.org/x/oauth2 v0.35.0 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 k8s.io/api v0.35.1 k8s.io/apiextensions-apiserver v0.35.1 k8s.io/apimachinery v0.35.1 k8s.io/client-go v0.35.1 k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 - sigs.k8s.io/controller-runtime v0.22.4 - sigs.k8s.io/multicluster-runtime v0.22.4-beta.1 + sigs.k8s.io/controller-runtime v0.23.1 + sigs.k8s.io/multicluster-runtime v0.23.1 sigs.k8s.io/yaml v1.6.0 ) @@ -40,7 +49,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect - github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect github.com/fluxcd/pkg/apis/kustomize v1.14.0 // indirect @@ -50,11 +59,13 @@ require ( github.com/getsentry/sentry-go v0.42.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect github.com/go-logr/zerologr v1.2.3 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -87,7 +98,6 @@ require ( github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/vektah/gqlparser/v2 v2.5.31 // indirect @@ -101,6 +111,7 @@ require ( go.opentelemetry.io/otel/sdk v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.47.0 // indirect @@ -122,5 +133,5 @@ require ( k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect ) diff --git a/go.sum b/go.sum index 927c5771..cf5f0089 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= github.com/99designs/gqlgen v0.17.86 h1:C8N3UTa5heXX6twl+b0AJyGkTwYL6dNmFrgZNLRcU6w= github.com/99designs/gqlgen v0.17.86/go.mod h1:KTrPl+vHA1IUzNlh4EYkl7+tcErL3MgKnhHrBcV74Fw= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= @@ -28,8 +28,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/evanphx/json-patch v5.8.0+incompatible h1:1Av9pn2FyxPdvrWNQszj1g6D6YthSmvCfcN6SYclTJg= github.com/evanphx/json-patch v5.8.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= @@ -76,6 +76,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -114,10 +116,12 @@ github.com/kcp-dev/apimachinery/v2 v2.30.0 h1:bj7lVVPJj5UnQFCWhXVAKC+eNaIMKGGxpq github.com/kcp-dev/apimachinery/v2 v2.30.0/go.mod h1:DOv0iw5tcgzFBhudwLFe2WHCLqtlgNkuO4AcqbZ4zVo= github.com/kcp-dev/logicalcluster/v3 v3.0.5 h1:JbYakokb+5Uinz09oTXomSUJVQsqfxEvU4RyHUYxHOU= github.com/kcp-dev/logicalcluster/v3 v3.0.5/go.mod h1:EWBUBxdr49fUB1cLMO4nOdBWmYifLbP1LfoL20KkXYY= -github.com/kcp-dev/multicluster-provider v0.4.0 h1:Segd0b2bTkBaSfodq3IFUbaUAA28S8KIl71W9Bftn3Y= -github.com/kcp-dev/multicluster-provider v0.4.0/go.mod h1:4QGU39wyNztoYNatdWqbdOV6/R9ZzaIh4DdSj30dm9o= +github.com/kcp-dev/multicluster-provider v0.5.0 h1:G5YW2POVftsnxUfK2vo7anX5R1I3gVjjNbo/4i5ttbo= +github.com/kcp-dev/multicluster-provider v0.5.0/go.mod h1:eJohrSXqLmpjfTSFBbZMoq4Osr57UKg9ZokvhCPNmHc= github.com/kcp-dev/sdk v0.30.0 h1:BdDiKJ7SeVfzLIxueQwbADTrH7bfZ7b5ACYSrx6P93Y= github.com/kcp-dev/sdk v0.30.0/go.mod h1:H3PkpM33QqwPMgGOOw3dfqbQ8dF2gu4NeIsufSlS5KE= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -159,8 +163,6 @@ github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/platform-mesh/account-operator v0.11.2 h1:+sb9bFoZDsh2LJJ+SGSakhhsMT6zYHV4Hiw1iHAvZJY= -github.com/platform-mesh/account-operator v0.11.2/go.mod h1:0h29j5KmFkbHjsGnj3LWyjYmQH3k5a8AUJDy+oDMqKs= github.com/platform-mesh/golang-commons v0.9.36 h1:vyUFSJNcu+X4+NOlxCqN3A/g3x94YZtqIlaQnIvxkUE= github.com/platform-mesh/golang-commons v0.9.36/go.mod h1:JQZtWcFL0THKN38EQIcwAdEnpUg1RRULuv6T161fjNA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -217,6 +219,8 @@ github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkW github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= @@ -247,18 +251,33 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20260211191109-2735e65f0518 h1:2E1CW7v5QB+Wi3N+MXllOtVR6SFmI8iJM8EdzgxrgrU= golang.org/x/exp v0.0.0-20260211191109-2735e65f0518/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -266,12 +285,22 @@ golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= @@ -280,8 +309,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1: google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -296,33 +325,33 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= -k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= +k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= -k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs= -k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4= -k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= -k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= -k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE= -k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs= +k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg= +k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ= +k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= +k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= +k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8= +k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= -sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= +sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= +sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/multicluster-runtime v0.22.4-beta.1 h1:0XWbDINepM9UOyLkqhG4g7BtGBFKCDvZFyPsw1vufKE= -sigs.k8s.io/multicluster-runtime v0.22.4-beta.1/go.mod h1:zSMb4mC8MAZK42l8eE1ywkeX6vjuNRenYzJ1w+GPdfI= +sigs.k8s.io/multicluster-runtime v0.23.1 h1:isjVh6zBuk/U1HjYm22knRZmFsn6sFinmyvV+/4puCc= +sigs.k8s.io/multicluster-runtime v0.23.1/go.mod h1:ri1Gvx7Qehy5nis6OnTgSpJIWaf2SuorHDwF/jvbWvM= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= From 0e9fc65049fa360d7cf5fb2eb12fb4278de8bd20 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 17 Feb 2026 13:54:02 +0000 Subject: [PATCH 46/61] fix: correct bogus command On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index b41f8172..a5d403b3 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -77,8 +77,7 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Account in parent account cluster: %w", err), true, true) } - // Append the stores tuples with every tuple for the Account not yet managed - // via the Store resource + // Ensure the necessary tuples in OpenFGA tuples, err := fga.TuplesForAccount(acc, ai, s.creatorRelation, s.parentRelation, s.objectType) if err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("building tuples for account: %w", err), true, true) From 28c234d4dee19f89a109116c60f97f07085c140d Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 17 Feb 2026 13:54:29 +0000 Subject: [PATCH 47/61] feat: add terminator command On-behalf-of: @SAP --- cmd/terminator.go | 137 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 cmd/terminator.go diff --git a/cmd/terminator.go b/cmd/terminator.go new file mode 100644 index 00000000..d67232ce --- /dev/null +++ b/cmd/terminator.go @@ -0,0 +1,137 @@ +package cmd + +import ( + "crypto/tls" + "os" + "strings" + + platformeshconfig "github.com/platform-mesh/golang-commons/config" + iclient "github.com/platform-mesh/security-operator/internal/client" + "github.com/platform-mesh/security-operator/internal/terminatingworkspaces" + + "github.com/platform-mesh/security-operator/internal/config" + "github.com/platform-mesh/security-operator/internal/controller" + "github.com/platform-mesh/security-operator/internal/predicates" + "github.com/spf13/cobra" + "github.com/spf13/viper" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/predicate" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + "k8s.io/client-go/rest" + + "github.com/kcp-dev/logicalcluster/v3" + kcptenancyv1alphav1 "github.com/kcp-dev/sdk/apis/tenancy/v1alpha1" + + mcclient "github.com/kcp-dev/multicluster-provider/client" +) + +var terminatorCfg config.Config + +var terminatorCmd = &cobra.Command{ + Use: "terminator", + Short: "FGA terminator for account workspaces", + RunE: func(cmd *cobra.Command, args []string) error { + kcpCfg, err := getKubeconfigFromPath(terminatorCfg.KCP.Kubeconfig) + if err != nil { + log.Error().Err(err).Msg("unable to get KCP kubeconfig") + os.Exit(1) + } + + mgrOpts := ctrl.Options{ + Scheme: scheme, + LeaderElection: defaultCfg.LeaderElection.Enabled, + LeaderElectionID: "security-operator-terminator.platform-mesh.io", + HealthProbeBindAddress: defaultCfg.HealthProbeBindAddress, + Metrics: server.Options{ + BindAddress: defaultCfg.Metrics.BindAddress, + TLSOpts: []func(*tls.Config){ + func(c *tls.Config) { + log.Info().Msg("disabling http/2") + c.NextProtos = []string{"http/1.1"} + }, + }, + }, + } + if defaultCfg.LeaderElection.Enabled { + inClusterCfg, err := rest.InClusterConfig() + if err != nil { + log.Error().Err(err).Msg("unable to create in-cluster config") + return err + } + mgrOpts.LeaderElectionConfig = inClusterCfg + } + + mcc, err := mcclient.New(kcpCfg, client.Options{Scheme: scheme}) + if err != nil { + log.Error().Err(err).Msg("Failed to create multicluster client") + os.Exit(1) + } + rootClient, err := iclient.NewForLogicalCluster(kcpCfg, scheme, logicalcluster.Name("root")) + if err != nil { + log.Error().Err(err).Msgf("Failed to get root client") + os.Exit(1) + } + var wt kcptenancyv1alphav1.WorkspaceType + if err := rootClient.Get(cmd.Context(), client.ObjectKey{ + Name: terminatorCfg.WorkspaceTypeName, + }, &wt); err != nil { + log.Error().Err(err).Msgf("Failed to get WorkspaceType %s", terminatorCfg.WorkspaceTypeName) + os.Exit(1) + } + if len(wt.Status.VirtualWorkspaces) == 0 { + log.Error().Err(err).Msgf("No VirtualWorkspaces found in WorkspaceType %s", terminatorCfg.WorkspaceTypeName) + os.Exit(1) + } + + virtualWorkspaceCfg := rest.CopyConfig(kcpCfg) + virtualWorkspaceCfg.Host = wt.Status.VirtualWorkspaces[0].URL + log.Info().Msgf("Created config with %s host", virtualWorkspaceCfg.Host) + + provider, err := terminatingworkspaces.New(kcpCfg, initializerCfg.WorkspaceTypeName, + terminatingworkspaces.Options{ + Scheme: mgrOpts.Scheme, + }, + ) + + mgr, err := mcmanager.New(kcpCfg, provider, mgrOpts) + if err != nil { + log.Error().Err(err).Msg("Failed to create manager") + os.Exit(1) + } + + if err := controller.NewAccountLogicalClusterTerminator(log, terminatorCfg, mcc, mgr). + SetupWithManager(mgr, defaultCfg, predicate.Not(predicates.LogicalClusterIsAccountTypeOrg())); err != nil { + log.Error().Err(err).Msg("Unable to create AccountLogicalClusterTerminator") + os.Exit(1) + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + log.Error().Err(err).Msg("unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + log.Error().Err(err).Msg("unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + + return mgr.Start(ctrl.SetupSignalHandler()) + }, +} + +func init() { + rootCmd.AddCommand(terminatorCmd) + + terminatorV := viper.NewWithOptions( + viper.EnvKeyReplacer(strings.NewReplacer("-", "_")), + ) + terminatorV.AutomaticEnv() + if err := platformeshconfig.BindConfigToFlags(terminatorV, terminatorCmd, &terminatorCfg); err != nil { + panic(err) + } +} From 8eb7fafe5f837818170b4505198c3e1b1c4761ed Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 17 Feb 2026 13:56:02 +0000 Subject: [PATCH 48/61] feat: add terminator controller and subroutine skaffolding On-behalf-of: @SAP --- cmd/root.go | 1 + internal/config/config.go | 4 + ...untlogicalcluster_terminator_controller.go | 53 +++++++++++++ .../subroutine/account_tuples_terminator.go | 69 +++++++++++++++++ internal/subroutine/remove_terminator.go | 77 +++++++++++++++++++ 5 files changed, 204 insertions(+) create mode 100644 internal/controller/accountlogicalcluster_terminator_controller.go create mode 100644 internal/subroutine/account_tuples_terminator.go create mode 100644 internal/subroutine/remove_terminator.go diff --git a/cmd/root.go b/cmd/root.go index d1f725e1..29e41b7c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -32,6 +32,7 @@ var rootCmd = &cobra.Command{ func init() { rootCmd.AddCommand(initializerCmd) + rootCmd.AddCommand(terminatorCmd) rootCmd.AddCommand(operatorCmd) rootCmd.AddCommand(modelGeneratorCmd) rootCmd.AddCommand(initContainerCmd) diff --git a/internal/config/config.go b/internal/config/config.go index 9424196a..e4e23658 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -64,3 +64,7 @@ type Config struct { func (config Config) InitializerName() string { return config.WorkspacePath + ":" + config.WorkspaceTypeName } + +func (config Config) TerminatorName() string { + return config.WorkspacePath + ":" + config.WorkspaceTypeName +} diff --git a/internal/controller/accountlogicalcluster_terminator_controller.go b/internal/controller/accountlogicalcluster_terminator_controller.go new file mode 100644 index 00000000..2f386d73 --- /dev/null +++ b/internal/controller/accountlogicalcluster_terminator_controller.go @@ -0,0 +1,53 @@ +package controller + +import ( + "context" + + platformeshconfig "github.com/platform-mesh/golang-commons/config" + "github.com/platform-mesh/golang-commons/controller/lifecycle/builder" + "github.com/platform-mesh/golang-commons/controller/lifecycle/multicluster" + lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" + "github.com/platform-mesh/golang-commons/logger" + "github.com/platform-mesh/security-operator/internal/config" + "github.com/platform-mesh/security-operator/internal/subroutine" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/predicate" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + + mcclient "github.com/kcp-dev/multicluster-provider/client" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" +) + +// AccountLogicalClusterTerminator acts as a terminator for account workspaces. +type AccountLogicalClusterTerminator struct { + log *logger.Logger + + mclifecycle *multicluster.LifecycleManager +} + +// NewAccountLogicalClusterTerminator returns a new AccountLogicalClusterTerminator. +func NewAccountLogicalClusterTerminator(log *logger.Logger, cfg config.Config, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountLogicalClusterTerminator { + return &AccountLogicalClusterTerminator{ + log: log, + mclifecycle: builder.NewBuilder("security", "AccountLogicalClusterTerminator", []lifecyclesubroutine.Subroutine{ + subroutine.NewAccountTuplesTerminatorSubroutine(mcc, mgr), + subroutine.NewRemoveTerminator(mgr, cfg), + }, log). + WithReadOnly(). + WithStaticThenExponentialRateLimiter(). + BuildMultiCluster(mgr), + } +} + +// Reconcile implements the reconcile logic. +func (r *AccountLogicalClusterTerminator) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) { + ctxWithCluster := mccontext.WithCluster(ctx, req.ClusterName) + return r.mclifecycle.Reconcile(ctxWithCluster, req, &kcpcorev1alpha1.LogicalCluster{}) +} + +// SetupWithManager registers the controller with the manager. +func (r *AccountLogicalClusterTerminator) SetupWithManager(mgr mcmanager.Manager, cfg *platformeshconfig.CommonServiceConfig, evp ...predicate.Predicate) error { + return r.mclifecycle.SetupWithManager(mgr, cfg.MaxConcurrentReconciles, "AccountLogicalClusterTerminator", &kcpcorev1alpha1.LogicalCluster{}, cfg.DebugLabelValue, r, r.log, evp...) +} diff --git a/internal/subroutine/account_tuples_terminator.go b/internal/subroutine/account_tuples_terminator.go new file mode 100644 index 00000000..938ffa34 --- /dev/null +++ b/internal/subroutine/account_tuples_terminator.go @@ -0,0 +1,69 @@ +package subroutine + +import ( + "context" + "fmt" + + "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" + lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" + "github.com/platform-mesh/golang-commons/errors" + "github.com/platform-mesh/golang-commons/logger" + ctrl "sigs.k8s.io/controller-runtime" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + mcclient "github.com/kcp-dev/multicluster-provider/client" + kcpcore "github.com/kcp-dev/sdk/apis/core" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" +) + +// AccountTuplesTerminatorSubroutine deletes FGA tuples when an account workspace +// is being terminated. +type AccountTuplesTerminatorSubroutine struct { + mgr mcmanager.Manager + mcc mcclient.ClusterClient + // fga, creatorRelation, parentRelation, objectType to be added +} + +// Process implements lifecycle.Subroutine. +func (s *AccountTuplesTerminatorSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + log := logger.LoadLoggerFromContext(ctx) + lc := instance.(*kcpcorev1alpha1.LogicalCluster) + + p := lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] + if p == "" { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster %s is not set", lc.Name), true, true) + } + lcID, ok := mccontext.ClusterFrom(ctx) + if !ok { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("cluster name not found in context"), true, true) + } + + log.Info().Msgf("Processing logical cluster %s with ID %s and path %s", lc.Name, lcID, p) + return ctrl.Result{}, nil +} + +// Finalize implements lifecycle.Subroutine. +func (s *AccountTuplesTerminatorSubroutine) Finalize(_ context.Context, _ runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + return ctrl.Result{}, nil +} + +// Finalizers implements lifecycle.Subroutine. +func (s *AccountTuplesTerminatorSubroutine) Finalizers(_ runtimeobject.RuntimeObject) []string { + return []string{} +} + +// GetName implements lifecycle.Subroutine. +func (s *AccountTuplesTerminatorSubroutine) GetName() string { + return "AccountTuplesTerminatorSubroutine" +} + +// NewAccountTuplesTerminatorSubroutine returns a new AccountTuplesTerminatorSubroutine. +func NewAccountTuplesTerminatorSubroutine(mcc mcclient.ClusterClient, mgr mcmanager.Manager /* fga, creatorRelation, parentRelation, objectType */) *AccountTuplesTerminatorSubroutine { + return &AccountTuplesTerminatorSubroutine{ + mgr: mgr, + mcc: mcc, + } +} + +var _ lifecyclesubroutine.Subroutine = &AccountTuplesTerminatorSubroutine{} diff --git a/internal/subroutine/remove_terminator.go b/internal/subroutine/remove_terminator.go new file mode 100644 index 00000000..edd1ac00 --- /dev/null +++ b/internal/subroutine/remove_terminator.go @@ -0,0 +1,77 @@ +package subroutine + +import ( + "context" + "fmt" + "slices" + + "github.com/kcp-dev/logicalcluster/v3" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" + "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" + "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" + "github.com/platform-mesh/golang-commons/errors" + "github.com/platform-mesh/golang-commons/logger" + iclient "github.com/platform-mesh/security-operator/internal/client" + "github.com/platform-mesh/security-operator/internal/config" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" +) + +type RemoveTerminatorSubroutine struct { + terminatorName string + mgr mcmanager.Manager +} + +// Finalize implements subroutine.Subroutine. +func (s *RemoveTerminatorSubroutine) Finalize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + _ = ctx + _ = instance + return ctrl.Result{}, nil +} + +// Finalizers implements subroutine.Subroutine. +func (s *RemoveTerminatorSubroutine) Finalizers(_ runtimeobject.RuntimeObject) []string { + return []string{} +} + +// GetName implements subroutine.Subroutine. +func (s *RemoveTerminatorSubroutine) GetName() string { return "RemoveTerminator" } + +// Process implements subroutine.Subroutine. +func (s *RemoveTerminatorSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + lc := *(instance.(*kcpcorev1alpha1.LogicalCluster)) + log := logger.LoadLoggerFromContext(ctx) + log.Info().Any("logicalcluster", instance).Msg("Running Process in Terminator") + + lcID, ok := mccontext.ClusterFrom(ctx) + if !ok { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("cluster name not found in context"), true, true) + } + lcClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(lcID)) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting client: %w", err), true, true) + } + + copy := lc.DeepCopy() + lc.Status.Terminators = slices.DeleteFunc(lc.Status.Terminators, func(t kcpcorev1alpha1.LogicalClusterTerminator) bool { + return t == kcpcorev1alpha1.LogicalClusterTerminator(s.terminatorName) + }) + + if err := lcClient.Status().Patch(ctx, &lc, client.MergeFrom(copy)); err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("patching LogicalCluster: %w", err), true, true) + } + + return ctrl.Result{}, nil +} + +// NewRemoveTerminator returns a new removeTerminator subroutine. +func NewRemoveTerminator(mgr mcmanager.Manager, cfg config.Config) *RemoveTerminatorSubroutine { + return &RemoveTerminatorSubroutine{ + terminatorName: cfg.TerminatorName(), + mgr: mgr, + } +} + +var _ subroutine.Subroutine = &RemoveTerminatorSubroutine{} From 7882db07a1fc67ea51efcec42641e84a38bffe75 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Tue, 17 Feb 2026 14:03:26 +0000 Subject: [PATCH 49/61] chore: regenerate mocks On-behalf-of: @SAP --- internal/subroutine/mocks/mock_CTRLManager.go | 110 +++++++++++++++++- internal/subroutine/mocks/mock_Client.go | 3 +- internal/subroutine/mocks/mock_Cluster.go | 59 +++++++++- .../mocks/mock_DiscoveryInterface.go | 1 - .../subroutine/remove_initializer_test.go | 5 + 5 files changed, 167 insertions(+), 11 deletions(-) diff --git a/internal/subroutine/mocks/mock_CTRLManager.go b/internal/subroutine/mocks/mock_CTRLManager.go index 642ad2bc..fbc68059 100644 --- a/internal/subroutine/mocks/mock_CTRLManager.go +++ b/internal/subroutine/mocks/mock_CTRLManager.go @@ -10,17 +10,18 @@ import ( "github.com/go-logr/logr" mock "github.com/stretchr/testify/mock" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/events" + "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/config" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/webhook" - - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/webhook/conversion" ) // NewCTRLManager creates a new instance of CTRLManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. @@ -546,6 +547,105 @@ func (_c *CTRLManager_GetControllerOptions_Call) RunAndReturn(run func() config. return _c } +// GetConverterRegistry provides a mock function for the type CTRLManager +func (_mock *CTRLManager) GetConverterRegistry() conversion.Registry { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for GetConverterRegistry") + } + + var r0 conversion.Registry + if returnFunc, ok := ret.Get(0).(func() conversion.Registry); ok { + r0 = returnFunc() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(conversion.Registry) + } + } + return r0 +} + +// CTRLManager_GetConverterRegistry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetConverterRegistry' +type CTRLManager_GetConverterRegistry_Call struct { + *mock.Call +} + +// GetConverterRegistry is a helper method to define mock.On call +func (_e *CTRLManager_Expecter) GetConverterRegistry() *CTRLManager_GetConverterRegistry_Call { + return &CTRLManager_GetConverterRegistry_Call{Call: _e.mock.On("GetConverterRegistry")} +} + +func (_c *CTRLManager_GetConverterRegistry_Call) Run(run func()) *CTRLManager_GetConverterRegistry_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CTRLManager_GetConverterRegistry_Call) Return(registry conversion.Registry) *CTRLManager_GetConverterRegistry_Call { + _c.Call.Return(registry) + return _c +} + +func (_c *CTRLManager_GetConverterRegistry_Call) RunAndReturn(run func() conversion.Registry) *CTRLManager_GetConverterRegistry_Call { + _c.Call.Return(run) + return _c +} + +// GetEventRecorder provides a mock function for the type CTRLManager +func (_mock *CTRLManager) GetEventRecorder(name string) events.EventRecorder { + ret := _mock.Called(name) + + if len(ret) == 0 { + panic("no return value specified for GetEventRecorder") + } + + var r0 events.EventRecorder + if returnFunc, ok := ret.Get(0).(func(string) events.EventRecorder); ok { + r0 = returnFunc(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(events.EventRecorder) + } + } + return r0 +} + +// CTRLManager_GetEventRecorder_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetEventRecorder' +type CTRLManager_GetEventRecorder_Call struct { + *mock.Call +} + +// GetEventRecorder is a helper method to define mock.On call +// - name string +func (_e *CTRLManager_Expecter) GetEventRecorder(name interface{}) *CTRLManager_GetEventRecorder_Call { + return &CTRLManager_GetEventRecorder_Call{Call: _e.mock.On("GetEventRecorder", name)} +} + +func (_c *CTRLManager_GetEventRecorder_Call) Run(run func(name string)) *CTRLManager_GetEventRecorder_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *CTRLManager_GetEventRecorder_Call) Return(v events.EventRecorder) *CTRLManager_GetEventRecorder_Call { + _c.Call.Return(v) + return _c +} + +func (_c *CTRLManager_GetEventRecorder_Call) RunAndReturn(run func(name string) events.EventRecorder) *CTRLManager_GetEventRecorder_Call { + _c.Call.Return(run) + return _c +} + // GetEventRecorderFor provides a mock function for the type CTRLManager func (_mock *CTRLManager) GetEventRecorderFor(name string) record.EventRecorder { ret := _mock.Called(name) diff --git a/internal/subroutine/mocks/mock_Client.go b/internal/subroutine/mocks/mock_Client.go index e17b5984..510476f5 100644 --- a/internal/subroutine/mocks/mock_Client.go +++ b/internal/subroutine/mocks/mock_Client.go @@ -8,11 +8,10 @@ import ( "context" mock "github.com/stretchr/testify/mock" - "sigs.k8s.io/controller-runtime/pkg/client" - "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" ) // NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. diff --git a/internal/subroutine/mocks/mock_Cluster.go b/internal/subroutine/mocks/mock_Cluster.go index ee0f1236..9a0fc522 100644 --- a/internal/subroutine/mocks/mock_Cluster.go +++ b/internal/subroutine/mocks/mock_Cluster.go @@ -9,13 +9,13 @@ import ( "net/http" mock "github.com/stretchr/testify/mock" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/client" - "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/events" "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" ) // NewMockCluster creates a new instance of MockCluster. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. @@ -229,6 +229,59 @@ func (_c *MockCluster_GetConfig_Call) RunAndReturn(run func() *rest.Config) *Moc return _c } +// GetEventRecorder provides a mock function for the type MockCluster +func (_mock *MockCluster) GetEventRecorder(name string) events.EventRecorder { + ret := _mock.Called(name) + + if len(ret) == 0 { + panic("no return value specified for GetEventRecorder") + } + + var r0 events.EventRecorder + if returnFunc, ok := ret.Get(0).(func(string) events.EventRecorder); ok { + r0 = returnFunc(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(events.EventRecorder) + } + } + return r0 +} + +// MockCluster_GetEventRecorder_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetEventRecorder' +type MockCluster_GetEventRecorder_Call struct { + *mock.Call +} + +// GetEventRecorder is a helper method to define mock.On call +// - name string +func (_e *MockCluster_Expecter) GetEventRecorder(name interface{}) *MockCluster_GetEventRecorder_Call { + return &MockCluster_GetEventRecorder_Call{Call: _e.mock.On("GetEventRecorder", name)} +} + +func (_c *MockCluster_GetEventRecorder_Call) Run(run func(name string)) *MockCluster_GetEventRecorder_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockCluster_GetEventRecorder_Call) Return(v events.EventRecorder) *MockCluster_GetEventRecorder_Call { + _c.Call.Return(v) + return _c +} + +func (_c *MockCluster_GetEventRecorder_Call) RunAndReturn(run func(name string) events.EventRecorder) *MockCluster_GetEventRecorder_Call { + _c.Call.Return(run) + return _c +} + // GetEventRecorderFor provides a mock function for the type MockCluster func (_mock *MockCluster) GetEventRecorderFor(name string) record.EventRecorder { ret := _mock.Called(name) diff --git a/internal/subroutine/mocks/mock_DiscoveryInterface.go b/internal/subroutine/mocks/mock_DiscoveryInterface.go index dd626a52..371e5925 100644 --- a/internal/subroutine/mocks/mock_DiscoveryInterface.go +++ b/internal/subroutine/mocks/mock_DiscoveryInterface.go @@ -7,7 +7,6 @@ package mocks import ( "github.com/google/gnostic-models/openapiv2" mock "github.com/stretchr/testify/mock" - "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/discovery" diff --git a/internal/subroutine/remove_initializer_test.go b/internal/subroutine/remove_initializer_test.go index 5ef69162..ddbb4d70 100644 --- a/internal/subroutine/remove_initializer_test.go +++ b/internal/subroutine/remove_initializer_test.go @@ -9,6 +9,7 @@ import ( "github.com/platform-mesh/security-operator/internal/subroutine/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" @@ -21,6 +22,10 @@ type fakeStatusWriter struct { err error } +func (f *fakeStatusWriter) Apply(context.Context, runtime.ApplyConfiguration, ...client.SubResourceApplyOption) error { + return nil +} + func (f *fakeStatusWriter) Create(ctx context.Context, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error { return nil } From 62030d385ab599f8eb1cea4cb74cf6689b5ab46b Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 18 Feb 2026 04:48:53 +0000 Subject: [PATCH 50/61] feat: delete tuples in terminator On-behalf-of: @SAP --- cmd/terminator.go | 12 +++- ...untlogicalcluster_terminator_controller.go | 5 +- internal/subroutine/account_tuples.go | 48 ++------------- internal/subroutine/account_tuples_common.go | 61 +++++++++++++++++++ .../subroutine/account_tuples_terminator.go | 38 +++++++----- 5 files changed, 103 insertions(+), 61 deletions(-) create mode 100644 internal/subroutine/account_tuples_common.go diff --git a/cmd/terminator.go b/cmd/terminator.go index d67232ce..847f1b20 100644 --- a/cmd/terminator.go +++ b/cmd/terminator.go @@ -5,9 +5,12 @@ import ( "os" "strings" + openfgav1 "github.com/openfga/api/proto/openfga/v1" platformeshconfig "github.com/platform-mesh/golang-commons/config" iclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/internal/terminatingworkspaces" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" "github.com/platform-mesh/security-operator/internal/config" "github.com/platform-mesh/security-operator/internal/controller" @@ -103,7 +106,14 @@ var terminatorCmd = &cobra.Command{ os.Exit(1) } - if err := controller.NewAccountLogicalClusterTerminator(log, terminatorCfg, mcc, mgr). + conn, err := grpc.NewClient(terminatorCfg.FGA.Target, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Error().Err(err).Msg("unable to create grpc client") + os.Exit(1) + } + fga := openfgav1.NewOpenFGAServiceClient(conn) + + if err := controller.NewAccountLogicalClusterTerminator(log, terminatorCfg, fga, mcc, mgr). SetupWithManager(mgr, defaultCfg, predicate.Not(predicates.LogicalClusterIsAccountTypeOrg())); err != nil { log.Error().Err(err).Msg("Unable to create AccountLogicalClusterTerminator") os.Exit(1) diff --git a/internal/controller/accountlogicalcluster_terminator_controller.go b/internal/controller/accountlogicalcluster_terminator_controller.go index 2f386d73..4aeaa83f 100644 --- a/internal/controller/accountlogicalcluster_terminator_controller.go +++ b/internal/controller/accountlogicalcluster_terminator_controller.go @@ -3,6 +3,7 @@ package controller import ( "context" + openfgav1 "github.com/openfga/api/proto/openfga/v1" platformeshconfig "github.com/platform-mesh/golang-commons/config" "github.com/platform-mesh/golang-commons/controller/lifecycle/builder" "github.com/platform-mesh/golang-commons/controller/lifecycle/multicluster" @@ -28,11 +29,11 @@ type AccountLogicalClusterTerminator struct { } // NewAccountLogicalClusterTerminator returns a new AccountLogicalClusterTerminator. -func NewAccountLogicalClusterTerminator(log *logger.Logger, cfg config.Config, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountLogicalClusterTerminator { +func NewAccountLogicalClusterTerminator(log *logger.Logger, cfg config.Config, fga openfgav1.OpenFGAServiceClient, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountLogicalClusterTerminator { return &AccountLogicalClusterTerminator{ log: log, mclifecycle: builder.NewBuilder("security", "AccountLogicalClusterTerminator", []lifecyclesubroutine.Subroutine{ - subroutine.NewAccountTuplesTerminatorSubroutine(mcc, mgr), + subroutine.NewAccountTuplesTerminatorSubroutine(mcc, mgr, fga, cfg.FGA.CreatorRelation, cfg.FGA.ParentRelation, cfg.FGA.ObjectType), subroutine.NewRemoveTerminator(mgr, cfg), }, log). WithReadOnly(). diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index a5d403b3..a0f4e3c1 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -5,23 +5,15 @@ import ( "fmt" openfgav1 "github.com/openfga/api/proto/openfga/v1" - accountsv1alpha1 "github.com/platform-mesh/account-operator/api/v1alpha1" "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/errors" "github.com/platform-mesh/golang-commons/logger" - iclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/pkg/fga" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" - kerrors "k8s.io/apimachinery/pkg/api/errors" - - "github.com/kcp-dev/logicalcluster/v3" mcclient "github.com/kcp-dev/multicluster-provider/client" - kcpcore "github.com/kcp-dev/sdk/apis/core" kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" ) @@ -40,44 +32,12 @@ type AccountTuplesSubroutine struct { // Process implements lifecycle.Subroutine. func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { lc := instance.(*kcpcorev1alpha1.LogicalCluster) - p := lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] - if p == "" { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster is not set"), true, true) - } - lcID, ok := mccontext.ClusterFrom(ctx) - if !ok { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("cluster name not found in context"), true, true) - } - - // The AccountInfo in the logical custer belongs to the Account the - // Workspace was created for - lcClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(lcID)) - if err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting client: %w", err), true, true) - } - var ai accountsv1alpha1.AccountInfo - if err := lcClient.Get(ctx, client.ObjectKey{ - Name: "account", - }, &ai); err != nil && !kerrors.IsNotFound(err) { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting AccountInfo for LogicalCluster: %w", err), true, true) - } else if kerrors.IsNotFound(err) { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("AccountInfo not found yet, requeueing"), true, false) - } - - // The actual Account resource belonging to the Workspace needs to be - // fetched from the parent Account's Workspace - parentAccountClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(ai.Spec.ParentAccount.Path)) - if err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting parent account cluster client: %w", err), true, true) - } - var acc accountsv1alpha1.Account - if err := parentAccountClient.Get(ctx, client.ObjectKey{ - Name: ai.Spec.Account.Name, - }, &acc); err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting Account in parent account cluster: %w", err), true, true) + acc, ai, opErr := AccountAndInfoForLogicalCluster(ctx, s.mgr, lc) + if opErr != nil { + return ctrl.Result{}, opErr } - // Ensure the necessary tuples in OpenFGA + // Ensure the necessary tuples in OpenFGA. tuples, err := fga.TuplesForAccount(acc, ai, s.creatorRelation, s.parentRelation, s.objectType) if err != nil { return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("building tuples for account: %w", err), true, true) diff --git a/internal/subroutine/account_tuples_common.go b/internal/subroutine/account_tuples_common.go new file mode 100644 index 00000000..efb908ee --- /dev/null +++ b/internal/subroutine/account_tuples_common.go @@ -0,0 +1,61 @@ +package subroutine + +import ( + "context" + "fmt" + + accountsv1alpha1 "github.com/platform-mesh/account-operator/api/v1alpha1" + "github.com/platform-mesh/golang-commons/errors" + iclient "github.com/platform-mesh/security-operator/internal/client" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + "github.com/kcp-dev/logicalcluster/v3" + kcpcore "github.com/kcp-dev/sdk/apis/core" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" +) + +// AccountAndInfoForLogicalCluster fetches the AccountInfo from the +// LogicalCluster and the corresponding Account from the parent account's +// workspace. +func AccountAndInfoForLogicalCluster(ctx context.Context, mgr mcmanager.Manager, lc *kcpcorev1alpha1.LogicalCluster) (accountsv1alpha1.Account, accountsv1alpha1.AccountInfo, errors.OperatorError) { + if lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] == "" { + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster is not set"), true, true) + } + lcID, ok := mccontext.ClusterFrom(ctx) + if !ok { + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("cluster name not found in context"), true, true) + } + + // The AccountInfo in the logical cluster belongs to the Account the + // Workspace was created for + lcClient, err := iclient.NewForLogicalCluster(mgr.GetLocalManager().GetConfig(), mgr.GetLocalManager().GetScheme(), logicalcluster.Name(lcID)) + if err != nil { + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("getting client: %w", err), true, true) + } + var ai accountsv1alpha1.AccountInfo + if err := lcClient.Get(ctx, client.ObjectKey{ + Name: "account", + }, &ai); err != nil && !kerrors.IsNotFound(err) { + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("getting AccountInfo for LogicalCluster: %w", err), true, true) + } else if kerrors.IsNotFound(err) { + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("AccountInfo not found yet, requeueing"), true, false) + } + + // The actual Account resource belonging to the Workspace needs to be + // fetched from the parent Account's Workspace + parentAccountClient, err := iclient.NewForLogicalCluster(mgr.GetLocalManager().GetConfig(), mgr.GetLocalManager().GetScheme(), logicalcluster.Name(ai.Spec.ParentAccount.Path)) + if err != nil { + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("getting parent account cluster client: %w", err), true, true) + } + var acc accountsv1alpha1.Account + if err := parentAccountClient.Get(ctx, client.ObjectKey{ + Name: ai.Spec.Account.Name, + }, &acc); err != nil { + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("getting Account in parent account cluster: %w", err), true, true) + } + + return acc, ai, nil +} diff --git a/internal/subroutine/account_tuples_terminator.go b/internal/subroutine/account_tuples_terminator.go index 938ffa34..205e4ba5 100644 --- a/internal/subroutine/account_tuples_terminator.go +++ b/internal/subroutine/account_tuples_terminator.go @@ -4,16 +4,16 @@ import ( "context" "fmt" + openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/errors" "github.com/platform-mesh/golang-commons/logger" + "github.com/platform-mesh/security-operator/pkg/fga" ctrl "sigs.k8s.io/controller-runtime" - mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" mcclient "github.com/kcp-dev/multicluster-provider/client" - kcpcore "github.com/kcp-dev/sdk/apis/core" kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" ) @@ -22,24 +22,30 @@ import ( type AccountTuplesTerminatorSubroutine struct { mgr mcmanager.Manager mcc mcclient.ClusterClient - // fga, creatorRelation, parentRelation, objectType to be added + fga openfgav1.OpenFGAServiceClient + + objectType string + parentRelation string + creatorRelation string } // Process implements lifecycle.Subroutine. func (s *AccountTuplesTerminatorSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { - log := logger.LoadLoggerFromContext(ctx) lc := instance.(*kcpcorev1alpha1.LogicalCluster) + acc, ai, opErr := AccountAndInfoForLogicalCluster(ctx, s.mgr, lc) + if opErr != nil { + return ctrl.Result{}, opErr + } - p := lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] - if p == "" { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster %s is not set", lc.Name), true, true) + // Delete the corresponding tuples in OpenFGA. + tuples, err := fga.TuplesForAccount(acc, ai, s.creatorRelation, s.parentRelation, s.objectType) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("building tuples for account: %w", err), true, true) } - lcID, ok := mccontext.ClusterFrom(ctx) - if !ok { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("cluster name not found in context"), true, true) + if err := fga.NewTupleManager(s.fga, ai.Spec.FGA.Store.Id, fga.AuthorizationModelIDLatest, logger.LoadLoggerFromContext(ctx)).Delete(ctx, tuples); err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("applying tuples for Account: %w", err), true, true) } - log.Info().Msgf("Processing logical cluster %s with ID %s and path %s", lc.Name, lcID, p) return ctrl.Result{}, nil } @@ -59,10 +65,14 @@ func (s *AccountTuplesTerminatorSubroutine) GetName() string { } // NewAccountTuplesTerminatorSubroutine returns a new AccountTuplesTerminatorSubroutine. -func NewAccountTuplesTerminatorSubroutine(mcc mcclient.ClusterClient, mgr mcmanager.Manager /* fga, creatorRelation, parentRelation, objectType */) *AccountTuplesTerminatorSubroutine { +func NewAccountTuplesTerminatorSubroutine(mcc mcclient.ClusterClient, mgr mcmanager.Manager, fga openfgav1.OpenFGAServiceClient, creatorRelation, parentRelation, objectType string) *AccountTuplesTerminatorSubroutine { return &AccountTuplesTerminatorSubroutine{ - mgr: mgr, - mcc: mcc, + mgr: mgr, + mcc: mcc, + fga: fga, + creatorRelation: creatorRelation, + parentRelation: parentRelation, + objectType: objectType, } } From 8307219a4dae50060c00beb4e447aa4549ca1fa7 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 18 Feb 2026 08:26:08 +0000 Subject: [PATCH 51/61] feat: implement terminator interface On-behalf-of: @SAP --- .../accountlogicalcluster_terminator_controller.go | 3 ++- internal/subroutine/account_tuples_terminator.go | 8 +++++++- internal/subroutine/remove_terminator.go | 11 +++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/internal/controller/accountlogicalcluster_terminator_controller.go b/internal/controller/accountlogicalcluster_terminator_controller.go index 4aeaa83f..12c634a6 100644 --- a/internal/controller/accountlogicalcluster_terminator_controller.go +++ b/internal/controller/accountlogicalcluster_terminator_controller.go @@ -33,8 +33,9 @@ func NewAccountLogicalClusterTerminator(log *logger.Logger, cfg config.Config, f return &AccountLogicalClusterTerminator{ log: log, mclifecycle: builder.NewBuilder("security", "AccountLogicalClusterTerminator", []lifecyclesubroutine.Subroutine{ - subroutine.NewAccountTuplesTerminatorSubroutine(mcc, mgr, fga, cfg.FGA.CreatorRelation, cfg.FGA.ParentRelation, cfg.FGA.ObjectType), + // Order will be reversed in the lifecycle manager subroutine.NewRemoveTerminator(mgr, cfg), + subroutine.NewAccountTuplesTerminatorSubroutine(mcc, mgr, fga, cfg.FGA.CreatorRelation, cfg.FGA.ParentRelation, cfg.FGA.ObjectType), }, log). WithReadOnly(). WithStaticThenExponentialRateLimiter(). diff --git a/internal/subroutine/account_tuples_terminator.go b/internal/subroutine/account_tuples_terminator.go index 205e4ba5..cdfd01f4 100644 --- a/internal/subroutine/account_tuples_terminator.go +++ b/internal/subroutine/account_tuples_terminator.go @@ -30,7 +30,12 @@ type AccountTuplesTerminatorSubroutine struct { } // Process implements lifecycle.Subroutine. -func (s *AccountTuplesTerminatorSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { +func (s *AccountTuplesTerminatorSubroutine) Process(_ context.Context, _ runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + return ctrl.Result{}, nil +} + +// Terminate implements lifecycle.Terminator. +func (s *AccountTuplesTerminatorSubroutine) Terminate(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { lc := instance.(*kcpcorev1alpha1.LogicalCluster) acc, ai, opErr := AccountAndInfoForLogicalCluster(ctx, s.mgr, lc) if opErr != nil { @@ -77,3 +82,4 @@ func NewAccountTuplesTerminatorSubroutine(mcc mcclient.ClusterClient, mgr mcmana } var _ lifecyclesubroutine.Subroutine = &AccountTuplesTerminatorSubroutine{} +var _ lifecyclesubroutine.Terminator = &AccountTuplesTerminatorSubroutine{} diff --git a/internal/subroutine/remove_terminator.go b/internal/subroutine/remove_terminator.go index edd1ac00..50cb6794 100644 --- a/internal/subroutine/remove_terminator.go +++ b/internal/subroutine/remove_terminator.go @@ -10,7 +10,6 @@ import ( "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/errors" - "github.com/platform-mesh/golang-commons/logger" iclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/internal/config" ctrl "sigs.k8s.io/controller-runtime" @@ -40,10 +39,13 @@ func (s *RemoveTerminatorSubroutine) Finalizers(_ runtimeobject.RuntimeObject) [ func (s *RemoveTerminatorSubroutine) GetName() string { return "RemoveTerminator" } // Process implements subroutine.Subroutine. -func (s *RemoveTerminatorSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { +func (s *RemoveTerminatorSubroutine) Process(_ context.Context, _ runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + return ctrl.Result{}, nil +} + +// Terminate implements subroutine.Terminator. +func (s *RemoveTerminatorSubroutine) Terminate(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { lc := *(instance.(*kcpcorev1alpha1.LogicalCluster)) - log := logger.LoadLoggerFromContext(ctx) - log.Info().Any("logicalcluster", instance).Msg("Running Process in Terminator") lcID, ok := mccontext.ClusterFrom(ctx) if !ok { @@ -75,3 +77,4 @@ func NewRemoveTerminator(mgr mcmanager.Manager, cfg config.Config) *RemoveTermin } var _ subroutine.Subroutine = &RemoveTerminatorSubroutine{} +var _ subroutine.Terminator = &RemoveTerminatorSubroutine{} From 1f00145942219b8553c50ae443d402165f5416bb Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 18 Feb 2026 08:29:10 +0000 Subject: [PATCH 52/61] feat: split docker kind tasks On-behalf-of: @SAP --- Taskfile.yaml | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/Taskfile.yaml b/Taskfile.yaml index 8cc14526..54447cd4 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -118,8 +118,8 @@ tasks: cmds: - go run ./cmd/main.go operator - docker:kind: - desc: "Build container image with current tag from kind cluster and load it" + docker:kind:load: + desc: "Build container image with current tag from kind cluster and load it into kind" vars: CONTAINER_RUNTIME: '{{.CONTAINER_RUNTIME | default "docker"}}' KIND_CLUSTER: '{{.KIND_CLUSTER | default "platform-mesh"}}' @@ -143,8 +143,30 @@ tasks: else kind load docker-image {{.IMAGE_NAME}} --name {{.KIND_CLUSTER}} fi + - echo "Image loaded into kind cluster {{.KIND_CLUSTER}}" + docker:kind:restart: + desc: "Restart security-operator deployments to pick up new image" + vars: + DEPLOYMENT_NAMESPACE: '{{.DEPLOYMENT_NAMESPACE | default "platform-mesh-system"}}' + cmds: - kubectl rollout restart deployment/security-operator -n {{.DEPLOYMENT_NAMESPACE}} - kubectl rollout restart deployment/security-operator-generator -n {{.DEPLOYMENT_NAMESPACE}} - kubectl rollout restart deployment/security-operator-initializer -n {{.DEPLOYMENT_NAMESPACE}} - - echo "Image loaded and all deployments restarted" + - kubectl rollout restart deployment/security-operator-terminator -n {{.DEPLOYMENT_NAMESPACE}} + - echo "All deployments restarted" + docker:kind: + desc: "Build container image, load it into kind cluster, and restart deployments" + vars: + DEPLOYMENT_NAMESPACE: '{{.DEPLOYMENT_NAMESPACE | default "platform-mesh-system"}}' + CONTAINER_RUNTIME: '{{.CONTAINER_RUNTIME | default "docker"}}' + KIND_CLUSTER: '{{.KIND_CLUSTER | default "platform-mesh"}}' + cmds: + - task: docker:kind:load + vars: + DEPLOYMENT_NAMESPACE: '{{.DEPLOYMENT_NAMESPACE}}' + CONTAINER_RUNTIME: '{{.CONTAINER_RUNTIME}}' + KIND_CLUSTER: '{{.KIND_CLUSTER}}' + - task: docker:kind:restart + vars: + DEPLOYMENT_NAMESPACE: '{{.DEPLOYMENT_NAMESPACE}}' From 466cb2f1d379952fea291c724b6bf6e3b272b079 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 18 Feb 2026 12:43:33 +0000 Subject: [PATCH 53/61] feat: tuples: implement initializer and terminator interfaces in a single subroutine On-behalf-of: @SAP --- .../accountlogicalcluster_controller.go | 1 + ...untlogicalcluster_terminator_controller.go | 5 +- internal/subroutine/account_tuples.go | 36 +++++++- .../subroutine/account_tuples_terminator.go | 85 ------------------- 4 files changed, 36 insertions(+), 91 deletions(-) delete mode 100644 internal/subroutine/account_tuples_terminator.go diff --git a/internal/controller/accountlogicalcluster_controller.go b/internal/controller/accountlogicalcluster_controller.go index a6b49e3f..f66a27c7 100644 --- a/internal/controller/accountlogicalcluster_controller.go +++ b/internal/controller/accountlogicalcluster_controller.go @@ -37,6 +37,7 @@ func NewAccountLogicalClusterReconciler(log *logger.Logger, cfg config.Config, f }, log). WithReadOnly(). WithStaticThenExponentialRateLimiter(). + WithInitializer(cfg.InitializerName()). BuildMultiCluster(mgr), } } diff --git a/internal/controller/accountlogicalcluster_terminator_controller.go b/internal/controller/accountlogicalcluster_terminator_controller.go index 12c634a6..d2d74f98 100644 --- a/internal/controller/accountlogicalcluster_terminator_controller.go +++ b/internal/controller/accountlogicalcluster_terminator_controller.go @@ -33,12 +33,11 @@ func NewAccountLogicalClusterTerminator(log *logger.Logger, cfg config.Config, f return &AccountLogicalClusterTerminator{ log: log, mclifecycle: builder.NewBuilder("security", "AccountLogicalClusterTerminator", []lifecyclesubroutine.Subroutine{ - // Order will be reversed in the lifecycle manager - subroutine.NewRemoveTerminator(mgr, cfg), - subroutine.NewAccountTuplesTerminatorSubroutine(mcc, mgr, fga, cfg.FGA.CreatorRelation, cfg.FGA.ParentRelation, cfg.FGA.ObjectType), + subroutine.NewAccountTuplesSubroutine(mcc, mgr, fga, cfg.FGA.CreatorRelation, cfg.FGA.ParentRelation, cfg.FGA.ObjectType), }, log). WithReadOnly(). WithStaticThenExponentialRateLimiter(). + WithTerminator(cfg.TerminatorName()). BuildMultiCluster(mgr), } } diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index a0f4e3c1..b951cf90 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -18,7 +18,7 @@ import ( ) // AccountTuplesSubroutine creates FGA tuples for Accounts not of the -// "org"-type. +// "org"-type when initializing, and deletes them when terminating. type AccountTuplesSubroutine struct { mgr mcmanager.Manager mcc mcclient.ClusterClient @@ -29,8 +29,14 @@ type AccountTuplesSubroutine struct { creatorRelation string } -// Process implements lifecycle.Subroutine. +// Process implements lifecycle.Subroutine as no-op since Initialize handles the +// work when not in deletion. func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + return ctrl.Result{}, nil +} + +// Initialize implements lifecycle.Initializer. +func (s *AccountTuplesSubroutine) Initialize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { lc := instance.(*kcpcorev1alpha1.LogicalCluster) acc, ai, opErr := AccountAndInfoForLogicalCluster(ctx, s.mgr, lc) if opErr != nil { @@ -49,6 +55,26 @@ func (s *AccountTuplesSubroutine) Process(ctx context.Context, instance runtimeo return ctrl.Result{}, nil } +// Terminate implements lifecycle.Terminator. +func (s *AccountTuplesSubroutine) Terminate(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + lc := instance.(*kcpcorev1alpha1.LogicalCluster) + acc, ai, opErr := AccountAndInfoForLogicalCluster(ctx, s.mgr, lc) + if opErr != nil { + return ctrl.Result{}, opErr + } + + // Delete the corresponding tuples in OpenFGA. + tuples, err := fga.TuplesForAccount(acc, ai, s.creatorRelation, s.parentRelation, s.objectType) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("building tuples for account: %w", err), true, true) + } + if err := fga.NewTupleManager(s.fga, ai.Spec.FGA.Store.Id, fga.AuthorizationModelIDLatest, logger.LoadLoggerFromContext(ctx)).Delete(ctx, tuples); err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("deleting tuples for Account: %w", err), true, true) + } + + return ctrl.Result{}, nil +} + // Finalize implements lifecycle.Subroutine. func (s *AccountTuplesSubroutine) Finalize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { return ctrl.Result{}, nil @@ -73,4 +99,8 @@ func NewAccountTuplesSubroutine(mcc mcclient.ClusterClient, mgr mcmanager.Manage } } -var _ lifecyclesubroutine.Subroutine = &AccountTuplesSubroutine{} +var ( + _ lifecyclesubroutine.Subroutine = &AccountTuplesSubroutine{} + _ lifecyclesubroutine.Initializer = &AccountTuplesSubroutine{} + _ lifecyclesubroutine.Terminator = &AccountTuplesSubroutine{} +) diff --git a/internal/subroutine/account_tuples_terminator.go b/internal/subroutine/account_tuples_terminator.go deleted file mode 100644 index cdfd01f4..00000000 --- a/internal/subroutine/account_tuples_terminator.go +++ /dev/null @@ -1,85 +0,0 @@ -package subroutine - -import ( - "context" - "fmt" - - openfgav1 "github.com/openfga/api/proto/openfga/v1" - "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" - lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" - "github.com/platform-mesh/golang-commons/errors" - "github.com/platform-mesh/golang-commons/logger" - "github.com/platform-mesh/security-operator/pkg/fga" - ctrl "sigs.k8s.io/controller-runtime" - mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" - - mcclient "github.com/kcp-dev/multicluster-provider/client" - kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" -) - -// AccountTuplesTerminatorSubroutine deletes FGA tuples when an account workspace -// is being terminated. -type AccountTuplesTerminatorSubroutine struct { - mgr mcmanager.Manager - mcc mcclient.ClusterClient - fga openfgav1.OpenFGAServiceClient - - objectType string - parentRelation string - creatorRelation string -} - -// Process implements lifecycle.Subroutine. -func (s *AccountTuplesTerminatorSubroutine) Process(_ context.Context, _ runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { - return ctrl.Result{}, nil -} - -// Terminate implements lifecycle.Terminator. -func (s *AccountTuplesTerminatorSubroutine) Terminate(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { - lc := instance.(*kcpcorev1alpha1.LogicalCluster) - acc, ai, opErr := AccountAndInfoForLogicalCluster(ctx, s.mgr, lc) - if opErr != nil { - return ctrl.Result{}, opErr - } - - // Delete the corresponding tuples in OpenFGA. - tuples, err := fga.TuplesForAccount(acc, ai, s.creatorRelation, s.parentRelation, s.objectType) - if err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("building tuples for account: %w", err), true, true) - } - if err := fga.NewTupleManager(s.fga, ai.Spec.FGA.Store.Id, fga.AuthorizationModelIDLatest, logger.LoadLoggerFromContext(ctx)).Delete(ctx, tuples); err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("applying tuples for Account: %w", err), true, true) - } - - return ctrl.Result{}, nil -} - -// Finalize implements lifecycle.Subroutine. -func (s *AccountTuplesTerminatorSubroutine) Finalize(_ context.Context, _ runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { - return ctrl.Result{}, nil -} - -// Finalizers implements lifecycle.Subroutine. -func (s *AccountTuplesTerminatorSubroutine) Finalizers(_ runtimeobject.RuntimeObject) []string { - return []string{} -} - -// GetName implements lifecycle.Subroutine. -func (s *AccountTuplesTerminatorSubroutine) GetName() string { - return "AccountTuplesTerminatorSubroutine" -} - -// NewAccountTuplesTerminatorSubroutine returns a new AccountTuplesTerminatorSubroutine. -func NewAccountTuplesTerminatorSubroutine(mcc mcclient.ClusterClient, mgr mcmanager.Manager, fga openfgav1.OpenFGAServiceClient, creatorRelation, parentRelation, objectType string) *AccountTuplesTerminatorSubroutine { - return &AccountTuplesTerminatorSubroutine{ - mgr: mgr, - mcc: mcc, - fga: fga, - creatorRelation: creatorRelation, - parentRelation: parentRelation, - objectType: objectType, - } -} - -var _ lifecyclesubroutine.Subroutine = &AccountTuplesTerminatorSubroutine{} -var _ lifecyclesubroutine.Terminator = &AccountTuplesTerminatorSubroutine{} From 47f8d56625ae2bfb039e240d8bc46b22372a3800 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 18 Feb 2026 13:02:51 +0000 Subject: [PATCH 54/61] refactor: unify account init and term controllers/subroutines On-behalf-of: @SAP --- cmd/terminator.go | 2 +- .../accountlogicalcluster_controller.go | 2 +- ...untlogicalcluster_terminator_controller.go | 54 ------------- internal/subroutine/account_tuples.go | 56 ++++++++++++- internal/subroutine/account_tuples_common.go | 61 -------------- internal/subroutine/remove_terminator.go | 80 ------------------- 6 files changed, 55 insertions(+), 200 deletions(-) delete mode 100644 internal/controller/accountlogicalcluster_terminator_controller.go delete mode 100644 internal/subroutine/account_tuples_common.go delete mode 100644 internal/subroutine/remove_terminator.go diff --git a/cmd/terminator.go b/cmd/terminator.go index 847f1b20..bb16d24d 100644 --- a/cmd/terminator.go +++ b/cmd/terminator.go @@ -113,7 +113,7 @@ var terminatorCmd = &cobra.Command{ } fga := openfgav1.NewOpenFGAServiceClient(conn) - if err := controller.NewAccountLogicalClusterTerminator(log, terminatorCfg, fga, mcc, mgr). + if err := controller.NewAccountLogicalClusterReconciler(log, terminatorCfg, fga, mcc, mgr). SetupWithManager(mgr, defaultCfg, predicate.Not(predicates.LogicalClusterIsAccountTypeOrg())); err != nil { log.Error().Err(err).Msg("Unable to create AccountLogicalClusterTerminator") os.Exit(1) diff --git a/internal/controller/accountlogicalcluster_controller.go b/internal/controller/accountlogicalcluster_controller.go index f66a27c7..f9641695 100644 --- a/internal/controller/accountlogicalcluster_controller.go +++ b/internal/controller/accountlogicalcluster_controller.go @@ -33,11 +33,11 @@ func NewAccountLogicalClusterReconciler(log *logger.Logger, cfg config.Config, f log: log, mclifecycle: builder.NewBuilder("security", "AccountLogicalClusterReconciler", []lifecyclesubroutine.Subroutine{ subroutine.NewAccountTuplesSubroutine(mcc, mgr, fga, cfg.FGA.CreatorRelation, cfg.FGA.ParentRelation, cfg.FGA.ObjectType), - subroutine.NewRemoveInitializer(mgr, cfg), }, log). WithReadOnly(). WithStaticThenExponentialRateLimiter(). WithInitializer(cfg.InitializerName()). + WithTerminator(cfg.TerminatorName()). BuildMultiCluster(mgr), } } diff --git a/internal/controller/accountlogicalcluster_terminator_controller.go b/internal/controller/accountlogicalcluster_terminator_controller.go deleted file mode 100644 index d2d74f98..00000000 --- a/internal/controller/accountlogicalcluster_terminator_controller.go +++ /dev/null @@ -1,54 +0,0 @@ -package controller - -import ( - "context" - - openfgav1 "github.com/openfga/api/proto/openfga/v1" - platformeshconfig "github.com/platform-mesh/golang-commons/config" - "github.com/platform-mesh/golang-commons/controller/lifecycle/builder" - "github.com/platform-mesh/golang-commons/controller/lifecycle/multicluster" - lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" - "github.com/platform-mesh/golang-commons/logger" - "github.com/platform-mesh/security-operator/internal/config" - "github.com/platform-mesh/security-operator/internal/subroutine" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/predicate" - mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" - mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" - mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" - - mcclient "github.com/kcp-dev/multicluster-provider/client" - kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" -) - -// AccountLogicalClusterTerminator acts as a terminator for account workspaces. -type AccountLogicalClusterTerminator struct { - log *logger.Logger - - mclifecycle *multicluster.LifecycleManager -} - -// NewAccountLogicalClusterTerminator returns a new AccountLogicalClusterTerminator. -func NewAccountLogicalClusterTerminator(log *logger.Logger, cfg config.Config, fga openfgav1.OpenFGAServiceClient, mcc mcclient.ClusterClient, mgr mcmanager.Manager) *AccountLogicalClusterTerminator { - return &AccountLogicalClusterTerminator{ - log: log, - mclifecycle: builder.NewBuilder("security", "AccountLogicalClusterTerminator", []lifecyclesubroutine.Subroutine{ - subroutine.NewAccountTuplesSubroutine(mcc, mgr, fga, cfg.FGA.CreatorRelation, cfg.FGA.ParentRelation, cfg.FGA.ObjectType), - }, log). - WithReadOnly(). - WithStaticThenExponentialRateLimiter(). - WithTerminator(cfg.TerminatorName()). - BuildMultiCluster(mgr), - } -} - -// Reconcile implements the reconcile logic. -func (r *AccountLogicalClusterTerminator) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) { - ctxWithCluster := mccontext.WithCluster(ctx, req.ClusterName) - return r.mclifecycle.Reconcile(ctxWithCluster, req, &kcpcorev1alpha1.LogicalCluster{}) -} - -// SetupWithManager registers the controller with the manager. -func (r *AccountLogicalClusterTerminator) SetupWithManager(mgr mcmanager.Manager, cfg *platformeshconfig.CommonServiceConfig, evp ...predicate.Predicate) error { - return r.mclifecycle.SetupWithManager(mgr, cfg.MaxConcurrentReconciles, "AccountLogicalClusterTerminator", &kcpcorev1alpha1.LogicalCluster{}, cfg.DebugLabelValue, r, r.log, evp...) -} diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index b951cf90..4ef73616 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -5,15 +5,22 @@ import ( "fmt" openfgav1 "github.com/openfga/api/proto/openfga/v1" + accountsv1alpha1 "github.com/platform-mesh/account-operator/api/v1alpha1" "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/errors" "github.com/platform-mesh/golang-commons/logger" + iclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/pkg/fga" + kerrors "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + "github.com/kcp-dev/logicalcluster/v3" mcclient "github.com/kcp-dev/multicluster-provider/client" + kcpcore "github.com/kcp-dev/sdk/apis/core" kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" ) @@ -100,7 +107,50 @@ func NewAccountTuplesSubroutine(mcc mcclient.ClusterClient, mgr mcmanager.Manage } var ( - _ lifecyclesubroutine.Subroutine = &AccountTuplesSubroutine{} - _ lifecyclesubroutine.Initializer = &AccountTuplesSubroutine{} - _ lifecyclesubroutine.Terminator = &AccountTuplesSubroutine{} + _ lifecyclesubroutine.Subroutine = &AccountTuplesSubroutine{} + _ lifecyclesubroutine.Initializer = &AccountTuplesSubroutine{} + _ lifecyclesubroutine.Terminator = &AccountTuplesSubroutine{} ) + +// AccountAndInfoForLogicalCluster fetches the AccountInfo from the +// LogicalCluster and the corresponding Account from the parent account's +// workspace. +func AccountAndInfoForLogicalCluster(ctx context.Context, mgr mcmanager.Manager, lc *kcpcorev1alpha1.LogicalCluster) (accountsv1alpha1.Account, accountsv1alpha1.AccountInfo, errors.OperatorError) { + if lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] == "" { + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster is not set"), true, true) + } + lcID, ok := mccontext.ClusterFrom(ctx) + if !ok { + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("cluster name not found in context"), true, true) + } + + // The AccountInfo in the logical cluster belongs to the Account the + // Workspace was created for + lcClient, err := iclient.NewForLogicalCluster(mgr.GetLocalManager().GetConfig(), mgr.GetLocalManager().GetScheme(), logicalcluster.Name(lcID)) + if err != nil { + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("getting client: %w", err), true, true) + } + var ai accountsv1alpha1.AccountInfo + if err := lcClient.Get(ctx, client.ObjectKey{ + Name: "account", + }, &ai); err != nil && !kerrors.IsNotFound(err) { + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("getting AccountInfo for LogicalCluster: %w", err), true, true) + } else if kerrors.IsNotFound(err) { + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("AccountInfo not found yet, requeueing"), true, false) + } + + // The actual Account resource belonging to the Workspace needs to be + // fetched from the parent Account's Workspace + parentAccountClient, err := iclient.NewForLogicalCluster(mgr.GetLocalManager().GetConfig(), mgr.GetLocalManager().GetScheme(), logicalcluster.Name(ai.Spec.ParentAccount.Path)) + if err != nil { + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("getting parent account cluster client: %w", err), true, true) + } + var acc accountsv1alpha1.Account + if err := parentAccountClient.Get(ctx, client.ObjectKey{ + Name: ai.Spec.Account.Name, + }, &acc); err != nil { + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("getting Account in parent account cluster: %w", err), true, true) + } + + return acc, ai, nil +} diff --git a/internal/subroutine/account_tuples_common.go b/internal/subroutine/account_tuples_common.go deleted file mode 100644 index efb908ee..00000000 --- a/internal/subroutine/account_tuples_common.go +++ /dev/null @@ -1,61 +0,0 @@ -package subroutine - -import ( - "context" - "fmt" - - accountsv1alpha1 "github.com/platform-mesh/account-operator/api/v1alpha1" - "github.com/platform-mesh/golang-commons/errors" - iclient "github.com/platform-mesh/security-operator/internal/client" - kerrors "k8s.io/apimachinery/pkg/api/errors" - "sigs.k8s.io/controller-runtime/pkg/client" - mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" - mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" - - "github.com/kcp-dev/logicalcluster/v3" - kcpcore "github.com/kcp-dev/sdk/apis/core" - kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" -) - -// AccountAndInfoForLogicalCluster fetches the AccountInfo from the -// LogicalCluster and the corresponding Account from the parent account's -// workspace. -func AccountAndInfoForLogicalCluster(ctx context.Context, mgr mcmanager.Manager, lc *kcpcorev1alpha1.LogicalCluster) (accountsv1alpha1.Account, accountsv1alpha1.AccountInfo, errors.OperatorError) { - if lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] == "" { - return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("annotation on LogicalCluster is not set"), true, true) - } - lcID, ok := mccontext.ClusterFrom(ctx) - if !ok { - return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("cluster name not found in context"), true, true) - } - - // The AccountInfo in the logical cluster belongs to the Account the - // Workspace was created for - lcClient, err := iclient.NewForLogicalCluster(mgr.GetLocalManager().GetConfig(), mgr.GetLocalManager().GetScheme(), logicalcluster.Name(lcID)) - if err != nil { - return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("getting client: %w", err), true, true) - } - var ai accountsv1alpha1.AccountInfo - if err := lcClient.Get(ctx, client.ObjectKey{ - Name: "account", - }, &ai); err != nil && !kerrors.IsNotFound(err) { - return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("getting AccountInfo for LogicalCluster: %w", err), true, true) - } else if kerrors.IsNotFound(err) { - return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("AccountInfo not found yet, requeueing"), true, false) - } - - // The actual Account resource belonging to the Workspace needs to be - // fetched from the parent Account's Workspace - parentAccountClient, err := iclient.NewForLogicalCluster(mgr.GetLocalManager().GetConfig(), mgr.GetLocalManager().GetScheme(), logicalcluster.Name(ai.Spec.ParentAccount.Path)) - if err != nil { - return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("getting parent account cluster client: %w", err), true, true) - } - var acc accountsv1alpha1.Account - if err := parentAccountClient.Get(ctx, client.ObjectKey{ - Name: ai.Spec.Account.Name, - }, &acc); err != nil { - return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("getting Account in parent account cluster: %w", err), true, true) - } - - return acc, ai, nil -} diff --git a/internal/subroutine/remove_terminator.go b/internal/subroutine/remove_terminator.go deleted file mode 100644 index 50cb6794..00000000 --- a/internal/subroutine/remove_terminator.go +++ /dev/null @@ -1,80 +0,0 @@ -package subroutine - -import ( - "context" - "fmt" - "slices" - - "github.com/kcp-dev/logicalcluster/v3" - kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" - "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" - "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" - "github.com/platform-mesh/golang-commons/errors" - iclient "github.com/platform-mesh/security-operator/internal/client" - "github.com/platform-mesh/security-operator/internal/config" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" - mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" -) - -type RemoveTerminatorSubroutine struct { - terminatorName string - mgr mcmanager.Manager -} - -// Finalize implements subroutine.Subroutine. -func (s *RemoveTerminatorSubroutine) Finalize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { - _ = ctx - _ = instance - return ctrl.Result{}, nil -} - -// Finalizers implements subroutine.Subroutine. -func (s *RemoveTerminatorSubroutine) Finalizers(_ runtimeobject.RuntimeObject) []string { - return []string{} -} - -// GetName implements subroutine.Subroutine. -func (s *RemoveTerminatorSubroutine) GetName() string { return "RemoveTerminator" } - -// Process implements subroutine.Subroutine. -func (s *RemoveTerminatorSubroutine) Process(_ context.Context, _ runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { - return ctrl.Result{}, nil -} - -// Terminate implements subroutine.Terminator. -func (s *RemoveTerminatorSubroutine) Terminate(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { - lc := *(instance.(*kcpcorev1alpha1.LogicalCluster)) - - lcID, ok := mccontext.ClusterFrom(ctx) - if !ok { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("cluster name not found in context"), true, true) - } - lcClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(lcID)) - if err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting client: %w", err), true, true) - } - - copy := lc.DeepCopy() - lc.Status.Terminators = slices.DeleteFunc(lc.Status.Terminators, func(t kcpcorev1alpha1.LogicalClusterTerminator) bool { - return t == kcpcorev1alpha1.LogicalClusterTerminator(s.terminatorName) - }) - - if err := lcClient.Status().Patch(ctx, &lc, client.MergeFrom(copy)); err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("patching LogicalCluster: %w", err), true, true) - } - - return ctrl.Result{}, nil -} - -// NewRemoveTerminator returns a new removeTerminator subroutine. -func NewRemoveTerminator(mgr mcmanager.Manager, cfg config.Config) *RemoveTerminatorSubroutine { - return &RemoveTerminatorSubroutine{ - terminatorName: cfg.TerminatorName(), - mgr: mgr, - } -} - -var _ subroutine.Subroutine = &RemoveTerminatorSubroutine{} -var _ subroutine.Terminator = &RemoveTerminatorSubroutine{} From a618566260bcfa439f742cf9beb4d8ccecda8647 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 18 Feb 2026 13:38:42 +0000 Subject: [PATCH 55/61] initializer: implement initalizer interface in all subroutines On-behalf-of: @SAP --- .../orglogicalcluster_controller.go | 3 +- internal/subroutine/idp.go | 12 ++++++- internal/subroutine/invite.go | 12 ++++++- internal/subroutine/remove_initializer.go | 12 +++++-- .../subroutine/workspace_authorization.go | 35 ++++++++++++------- internal/subroutine/workspace_initializer.go | 12 ++++++- 6 files changed, 66 insertions(+), 20 deletions(-) diff --git a/internal/controller/orglogicalcluster_controller.go b/internal/controller/orglogicalcluster_controller.go index 45a6abe7..6840f8c6 100644 --- a/internal/controller/orglogicalcluster_controller.go +++ b/internal/controller/orglogicalcluster_controller.go @@ -41,14 +41,13 @@ func NewOrgLogicalClusterReconciler(log *logger.Logger, orgClient client.Client, if cfg.Initializer.WorkspaceAuthEnabled { subroutines = append(subroutines, subroutine.NewWorkspaceAuthConfigurationSubroutine(orgClient, inClusterClient, mgr, cfg)) } - // RemoveInitializer is always included - it's the final cleanup step - subroutines = append(subroutines, subroutine.NewRemoveInitializer(mgr, cfg)) return &OrgLogicalClusterReconciler{ log: log, mclifecycle: builder.NewBuilder("logicalcluster", "OrgLogicalClusterReconciler", subroutines, log). WithReadOnly(). WithStaticThenExponentialRateLimiter(). + WithInitializer(cfg.InitializerName()). BuildMultiCluster(mgr), } } diff --git a/internal/subroutine/idp.go b/internal/subroutine/idp.go index e0312e0d..6ffae6fc 100644 --- a/internal/subroutine/idp.go +++ b/internal/subroutine/idp.go @@ -48,7 +48,10 @@ func NewIDPSubroutine(orgsClient client.Client, mgr mcmanager.Manager, cfg confi } } -var _ lifecyclesubroutine.Subroutine = &IDPSubroutine{} +var ( + _ lifecyclesubroutine.Subroutine = &IDPSubroutine{} + _ lifecyclesubroutine.Initializer = &IDPSubroutine{} +) type IDPSubroutine struct { orgsClient client.Client @@ -70,7 +73,14 @@ func (i *IDPSubroutine) Finalizers(_ runtimeobject.RuntimeObject) []string { func (i *IDPSubroutine) GetName() string { return "IDPSubroutine" } +// Process implements lifecycle.Subroutine as no-op since Initialize handles the +// work. func (i *IDPSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + return ctrl.Result{}, nil +} + +// Initialize implements lifecycle.Initializer. +func (i *IDPSubroutine) Initialize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { lc := instance.(*kcpcorev1alpha1.LogicalCluster) workspaceName := getWorkspaceName(lc) diff --git a/internal/subroutine/invite.go b/internal/subroutine/invite.go index 766c7f74..d4d6f1d5 100644 --- a/internal/subroutine/invite.go +++ b/internal/subroutine/invite.go @@ -31,7 +31,10 @@ func NewInviteSubroutine(orgsClient client.Client, mgr mcmanager.Manager) *invit } } -var _ lifecyclesubroutine.Subroutine = &inviteSubroutine{} +var ( + _ lifecyclesubroutine.Subroutine = &inviteSubroutine{} + _ lifecyclesubroutine.Initializer = &inviteSubroutine{} +) type inviteSubroutine struct { orgsClient client.Client @@ -48,7 +51,14 @@ func (w *inviteSubroutine) Finalizers(_ runtimeobject.RuntimeObject) []string { func (w *inviteSubroutine) GetName() string { return "InviteInitilizationSubroutine" } +// Process implements lifecycle.Subroutine as no-op since Initialize handles the +// work. func (w *inviteSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + return ctrl.Result{}, nil +} + +// Initialize implements lifecycle.Initializer. +func (w *inviteSubroutine) Initialize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { lc := instance.(*kcpcorev1alpha1.LogicalCluster) wsName := getWorkspaceName(lc) diff --git a/internal/subroutine/remove_initializer.go b/internal/subroutine/remove_initializer.go index 5356720e..bce18ffc 100644 --- a/internal/subroutine/remove_initializer.go +++ b/internal/subroutine/remove_initializer.go @@ -34,8 +34,13 @@ func (r *removeInitializer) Finalizers(_ runtimeobject.RuntimeObject) []string { // GetName implements subroutine.Subroutine. func (r *removeInitializer) GetName() string { return "RemoveInitializer" } -// Process implements subroutine.Subroutine. +// Process implements subroutine.Subroutine as no-op since Initialize handles the work. func (r *removeInitializer) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + return ctrl.Result{}, nil +} + +// Initialize implements lifecycle.Initializer. +func (r *removeInitializer) Initialize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { lc := instance.(*kcpcorev1alpha1.LogicalCluster) initializer := kcpcorev1alpha1.LogicalClusterInitializer(r.initializerName) @@ -69,4 +74,7 @@ func NewRemoveInitializer(mgr mcmanager.Manager, cfg config.Config) *removeIniti } } -var _ subroutine.Subroutine = &removeInitializer{} +var ( + _ subroutine.Subroutine = &removeInitializer{} + _ subroutine.Initializer = &removeInitializer{} +) diff --git a/internal/subroutine/workspace_authorization.go b/internal/subroutine/workspace_authorization.go index bd445957..b2dd282f 100644 --- a/internal/subroutine/workspace_authorization.go +++ b/internal/subroutine/workspace_authorization.go @@ -13,7 +13,6 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/reconcile" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" corev1 "k8s.io/api/core/v1" @@ -42,7 +41,10 @@ func NewWorkspaceAuthConfigurationSubroutine(orgClient, runtimeClient client.Cli } } -var _ lifecyclesubroutine.Subroutine = &workspaceAuthSubroutine{} +var ( + _ lifecyclesubroutine.Subroutine = &workspaceAuthSubroutine{} + _ lifecyclesubroutine.Initializer = &workspaceAuthSubroutine{} +) func (r *workspaceAuthSubroutine) GetName() string { return "workspaceAuthConfiguration" } @@ -50,11 +52,18 @@ func (r *workspaceAuthSubroutine) Finalizers(_ runtimeobject.RuntimeObject) []st return []string{} } -func (r *workspaceAuthSubroutine) Finalize(ctx context.Context, instance runtimeobject.RuntimeObject) (reconcile.Result, errors.OperatorError) { - return reconcile.Result{}, nil +func (r *workspaceAuthSubroutine) Finalize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + return ctrl.Result{}, nil +} + +// Process implements lifecycle.Subroutine as no-op since Initialize handles the +// work. +func (r *workspaceAuthSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + return ctrl.Result{}, nil } -func (r *workspaceAuthSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (reconcile.Result, errors.OperatorError) { +// Initialize implements lifecycle.Initializer. +func (r *workspaceAuthSubroutine) Initialize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { lc := instance.(*kcpcorev1alpha1.LogicalCluster) workspaceName := getWorkspaceName(lc) @@ -66,33 +75,33 @@ func (r *workspaceAuthSubroutine) Process(ctx context.Context, instance runtimeo if r.cfg.DomainCALookup { err := r.runtimeClient.Get(ctx, client.ObjectKey{Name: "domain-certificate-ca", Namespace: "platform-mesh-system"}, &domainCASecret) if err != nil { - return reconcile.Result{}, errors.NewOperatorError(fmt.Errorf("failed to get domain CA secret: %w", err), true, false) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("failed to get domain CA secret: %w", err), true, false) } } cluster, err := r.mgr.ClusterFromContext(ctx) if err != nil { - return reconcile.Result{}, errors.NewOperatorError(fmt.Errorf("failed to get cluster from context %w", err), true, true) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("failed to get cluster from context %w", err), true, true) } var idpConfig v1alpha1.IdentityProviderConfiguration err = cluster.GetClient().Get(ctx, types.NamespacedName{Name: workspaceName}, &idpConfig) if err != nil { - return reconcile.Result{}, errors.NewOperatorError(fmt.Errorf("failed to get IdentityProviderConfiguration: %w", err), true, true) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("failed to get IdentityProviderConfiguration: %w", err), true, true) } if len(idpConfig.Spec.Clients) == 0 || len(idpConfig.Status.ManagedClients) == 0 { - return reconcile.Result{}, errors.NewOperatorError(fmt.Errorf("IdentityProviderConfiguration %s has no clients in spec or status", workspaceName), true, false) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("IdentityProviderConfiguration %s has no clients in spec or status", workspaceName), true, false) } audiences := make([]string, 0, len(idpConfig.Spec.Clients)) for _, specClient := range idpConfig.Spec.Clients { managedClient, ok := idpConfig.Status.ManagedClients[specClient.ClientName] if !ok { - return reconcile.Result{}, errors.NewOperatorError(fmt.Errorf("managed client %s not found in IdentityProviderConfiguration status", specClient.ClientName), true, false) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("managed client %s not found in IdentityProviderConfiguration status", specClient.ClientName), true, false) } if managedClient.ClientID == "" { - return reconcile.Result{}, errors.NewOperatorError(fmt.Errorf("managed client %s has empty ClientID in IdentityProviderConfiguration status", specClient.ClientName), true, false) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("managed client %s has empty ClientID in IdentityProviderConfiguration status", specClient.ClientName), true, false) } audiences = append(audiences, managedClient.ClientID) } @@ -146,12 +155,12 @@ func (r *workspaceAuthSubroutine) Process(ctx context.Context, instance runtimeo return nil }) if err != nil { - return reconcile.Result{}, errors.NewOperatorError(fmt.Errorf("failed to create WorkspaceAuthConfiguration resource: %w", err), true, true) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("failed to create WorkspaceAuthConfiguration resource: %w", err), true, true) } err = r.patchWorkspaceTypes(ctx, r.orgClient, workspaceName) if err != nil { - return reconcile.Result{}, errors.NewOperatorError(fmt.Errorf("failed to patch workspace types: %w", err), true, true) + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("failed to patch workspace types: %w", err), true, true) } return ctrl.Result{}, nil diff --git a/internal/subroutine/workspace_initializer.go b/internal/subroutine/workspace_initializer.go index b1c27e8e..dba9b64c 100644 --- a/internal/subroutine/workspace_initializer.go +++ b/internal/subroutine/workspace_initializer.go @@ -47,7 +47,10 @@ func NewWorkspaceInitializer(orgsClient client.Client, cfg config.Config, mgr mc } } -var _ lifecyclesubroutine.Subroutine = &workspaceInitializer{} +var ( + _ lifecyclesubroutine.Subroutine = &workspaceInitializer{} + _ lifecyclesubroutine.Initializer = &workspaceInitializer{} +) type workspaceInitializer struct { orgsClient client.Client @@ -71,7 +74,14 @@ func (w *workspaceInitializer) Finalizers(_ runtimeobject.RuntimeObject) []strin func (w *workspaceInitializer) GetName() string { return "WorkspaceInitializer" } +// Process implements lifecycle.Subroutine as no-op since Initialize handles the +// work. func (w *workspaceInitializer) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { + return ctrl.Result{}, nil +} + +// Initialize implements lifecycle.Initializer. +func (w *workspaceInitializer) Initialize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { lc := instance.(*kcpcorev1alpha1.LogicalCluster) p := lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey] if p == "" { From 385605ed856545a8314de2e7cc26a111de305d14 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 18 Feb 2026 13:42:16 +0000 Subject: [PATCH 56/61] refactor: break comment On-behalf-of: @SAP --- internal/subroutine/remove_initializer.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/subroutine/remove_initializer.go b/internal/subroutine/remove_initializer.go index bce18ffc..98dd730f 100644 --- a/internal/subroutine/remove_initializer.go +++ b/internal/subroutine/remove_initializer.go @@ -34,7 +34,8 @@ func (r *removeInitializer) Finalizers(_ runtimeobject.RuntimeObject) []string { // GetName implements subroutine.Subroutine. func (r *removeInitializer) GetName() string { return "RemoveInitializer" } -// Process implements subroutine.Subroutine as no-op since Initialize handles the work. +// Process implements subroutine.Subroutine as no-op since Initialize handles +// the work. func (r *removeInitializer) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { return ctrl.Result{}, nil } From ed439360eca14b73ff9ca726b9e75eec0a6900d3 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 18 Feb 2026 13:42:48 +0000 Subject: [PATCH 57/61] fix: remove now unused removeInitializer subroutine On-behalf-of: @SAP --- internal/subroutine/remove_initializer.go | 81 ----------- .../subroutine/remove_initializer_test.go | 129 ------------------ 2 files changed, 210 deletions(-) delete mode 100644 internal/subroutine/remove_initializer.go delete mode 100644 internal/subroutine/remove_initializer_test.go diff --git a/internal/subroutine/remove_initializer.go b/internal/subroutine/remove_initializer.go deleted file mode 100644 index 98dd730f..00000000 --- a/internal/subroutine/remove_initializer.go +++ /dev/null @@ -1,81 +0,0 @@ -package subroutine - -import ( - "context" - "fmt" - "slices" - - "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" - "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" - "github.com/platform-mesh/golang-commons/errors" - "github.com/platform-mesh/security-operator/internal/config" - "github.com/rs/zerolog/log" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" - - "github.com/kcp-dev/sdk/apis/cache/initialization" - kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" -) - -type removeInitializer struct { - initializerName string - mgr mcmanager.Manager -} - -// Finalize implements subroutine.Subroutine. -func (r *removeInitializer) Finalize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { - return ctrl.Result{}, nil -} - -// Finalizers implements subroutine.Subroutine. -func (r *removeInitializer) Finalizers(_ runtimeobject.RuntimeObject) []string { return []string{} } - -// GetName implements subroutine.Subroutine. -func (r *removeInitializer) GetName() string { return "RemoveInitializer" } - -// Process implements subroutine.Subroutine as no-op since Initialize handles -// the work. -func (r *removeInitializer) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { - return ctrl.Result{}, nil -} - -// Initialize implements lifecycle.Initializer. -func (r *removeInitializer) Initialize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) { - lc := instance.(*kcpcorev1alpha1.LogicalCluster) - - initializer := kcpcorev1alpha1.LogicalClusterInitializer(r.initializerName) - - cluster, err := r.mgr.ClusterFromContext(ctx) - if err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("unable to get cluster from context: %w", err), true, false) - } - - if !slices.Contains(lc.Status.Initializers, initializer) { - log.Info().Msg("Initializer already absent, skipping patch") - return ctrl.Result{}, nil - } - - patch := client.MergeFrom(lc.DeepCopy()) - - lc.Status.Initializers = initialization.EnsureInitializerAbsent(initializer, lc.Status.Initializers) - if err := cluster.GetClient().Status().Patch(ctx, lc, patch); err != nil { - return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("unable to patch out initializers: %w", err), true, true) - } - - log.Info().Msg(fmt.Sprintf("Removed initializer from LogicalCluster status, name %s,uuid %s", lc.Name, lc.UID)) - - return ctrl.Result{}, nil -} - -func NewRemoveInitializer(mgr mcmanager.Manager, cfg config.Config) *removeInitializer { - return &removeInitializer{ - initializerName: cfg.InitializerName(), - mgr: mgr, - } -} - -var ( - _ subroutine.Subroutine = &removeInitializer{} - _ subroutine.Initializer = &removeInitializer{} -) diff --git a/internal/subroutine/remove_initializer_test.go b/internal/subroutine/remove_initializer_test.go deleted file mode 100644 index ddbb4d70..00000000 --- a/internal/subroutine/remove_initializer_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package subroutine_test - -import ( - "context" - "testing" - - "github.com/platform-mesh/security-operator/internal/config" - "github.com/platform-mesh/security-operator/internal/subroutine" - "github.com/platform-mesh/security-operator/internal/subroutine/mocks" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - - kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" -) - -// fakeStatusWriter implements client.SubResourceWriter to intercept Status().Patch calls -type fakeStatusWriter struct { - t *testing.T - expectClear kcpcorev1alpha1.LogicalClusterInitializer - err error -} - -func (f *fakeStatusWriter) Apply(context.Context, runtime.ApplyConfiguration, ...client.SubResourceApplyOption) error { - return nil -} - -func (f *fakeStatusWriter) Create(ctx context.Context, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error { - return nil -} - -func (f *fakeStatusWriter) Update(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error { - return nil -} - -func (f *fakeStatusWriter) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error { - lc := obj.(*kcpcorev1alpha1.LogicalCluster) - // Ensure initializer was removed before patch - for _, init := range lc.Status.Initializers { - if init == f.expectClear { - f.t.Fatalf("initializer %q should have been removed prior to Patch", string(init)) - } - } - return f.err -} - -func TestRemoveInitializer_Process(t *testing.T) { - cfg := config.Config{ - WorkspacePath: "root", - WorkspaceTypeName: "foo.initializer.kcp.dev", - } - initializerName := cfg.InitializerName() - - t.Run("skips when initializer is absent", func(t *testing.T) { - mgr := mocks.NewMockManager(t) - cluster := mocks.NewMockCluster(t) - mgr.EXPECT().ClusterFromContext(mock.Anything).Return(cluster, nil) - - lc := &kcpcorev1alpha1.LogicalCluster{} - lc.Status.Initializers = []kcpcorev1alpha1.LogicalClusterInitializer{"other.initializer"} - - r := subroutine.NewRemoveInitializer(mgr, cfg) - _, err := r.Process(context.Background(), lc) - assert.Nil(t, err) - }) - - t.Run("removes initializer and patches status", func(t *testing.T) { - mgr := mocks.NewMockManager(t) - cluster := mocks.NewMockCluster(t) - k8s := mocks.NewMockClient(t) - - mgr.EXPECT().ClusterFromContext(mock.Anything).Return(cluster, nil) - cluster.EXPECT().GetClient().Return(k8s) - k8s.EXPECT().Status().Return(&fakeStatusWriter{t: t, expectClear: kcpcorev1alpha1.LogicalClusterInitializer(initializerName), err: nil}) - - lc := &kcpcorev1alpha1.LogicalCluster{} - lc.Status.Initializers = []kcpcorev1alpha1.LogicalClusterInitializer{ - kcpcorev1alpha1.LogicalClusterInitializer(initializerName), - "another.initializer", - } - - r := subroutine.NewRemoveInitializer(mgr, cfg) - _, err := r.Process(context.Background(), lc) - assert.Nil(t, err) - for _, init := range lc.Status.Initializers { - assert.NotEqual(t, initializerName, string(init)) - } - }) - - t.Run("returns error when status patch fails", func(t *testing.T) { - mgr := mocks.NewMockManager(t) - cluster := mocks.NewMockCluster(t) - k8s := mocks.NewMockClient(t) - - mgr.EXPECT().ClusterFromContext(mock.Anything).Return(cluster, nil) - cluster.EXPECT().GetClient().Return(k8s) - k8s.EXPECT().Status().Return(&fakeStatusWriter{t: t, expectClear: kcpcorev1alpha1.LogicalClusterInitializer(initializerName), err: assert.AnError}) - - lc := &kcpcorev1alpha1.LogicalCluster{} - lc.Status.Initializers = []kcpcorev1alpha1.LogicalClusterInitializer{ - kcpcorev1alpha1.LogicalClusterInitializer(initializerName), - } - - r := subroutine.NewRemoveInitializer(mgr, cfg) - _, err := r.Process(context.Background(), lc) - assert.NotNil(t, err) - }) -} - -func TestRemoveInitializer_Misc(t *testing.T) { - mgr := mocks.NewMockManager(t) - r := subroutine.NewRemoveInitializer(mgr, config.Config{WorkspacePath: "root", WorkspaceTypeName: "foo.initializer.kcp.dev"}) - - assert.Equal(t, "RemoveInitializer", r.GetName()) - assert.Equal(t, []string{}, r.Finalizers(nil)) - - _, err := r.Finalize(context.Background(), &kcpcorev1alpha1.LogicalCluster{}) - assert.Nil(t, err) -} - -func TestRemoveInitializer_ManagerError(t *testing.T) { - mgr := mocks.NewMockManager(t) - mgr.EXPECT().ClusterFromContext(mock.Anything).Return(nil, assert.AnError) - - r := subroutine.NewRemoveInitializer(mgr, config.Config{WorkspacePath: "root", WorkspaceTypeName: "foo.initializer.kcp.dev"}) - _, err := r.Process(context.Background(), &kcpcorev1alpha1.LogicalCluster{}) - assert.NotNil(t, err) -} From 600a72f98c5fb2a8e3474272743d22e20553df89 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Wed, 18 Feb 2026 13:45:46 +0000 Subject: [PATCH 58/61] fix: change error wrt not found On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index 4ef73616..0324bbec 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -136,7 +136,7 @@ func AccountAndInfoForLogicalCluster(ctx context.Context, mgr mcmanager.Manager, }, &ai); err != nil && !kerrors.IsNotFound(err) { return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("getting AccountInfo for LogicalCluster: %w", err), true, true) } else if kerrors.IsNotFound(err) { - return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("AccountInfo not found yet, requeueing"), true, false) + return accountsv1alpha1.Account{}, accountsv1alpha1.AccountInfo{}, errors.NewOperatorError(fmt.Errorf("AccountInfo not found"), true, true) } // The actual Account resource belonging to the Workspace needs to be From 7956cd607f2f46a4309fcbf40d7e0d3e686881a6 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Thu, 19 Feb 2026 05:00:48 +0000 Subject: [PATCH 59/61] fix: remove bogus variable and use terminatorCFg On-behalf-of: @SAP --- cmd/terminator.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cmd/terminator.go b/cmd/terminator.go index bb16d24d..58fe5f56 100644 --- a/cmd/terminator.go +++ b/cmd/terminator.go @@ -90,11 +90,7 @@ var terminatorCmd = &cobra.Command{ os.Exit(1) } - virtualWorkspaceCfg := rest.CopyConfig(kcpCfg) - virtualWorkspaceCfg.Host = wt.Status.VirtualWorkspaces[0].URL - log.Info().Msgf("Created config with %s host", virtualWorkspaceCfg.Host) - - provider, err := terminatingworkspaces.New(kcpCfg, initializerCfg.WorkspaceTypeName, + provider, err := terminatingworkspaces.New(kcpCfg, terminatorCfg.WorkspaceTypeName, terminatingworkspaces.Options{ Scheme: mgrOpts.Scheme, }, From 60e0232a0f6b01a24a9c9f652db5d13a18d4c500 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Thu, 19 Feb 2026 06:30:57 +0000 Subject: [PATCH 60/61] refactor: use NewForAllPlatformMesh where applicable On-behalf-of: @SAP --- cmd/model_generator.go | 2 +- cmd/operator.go | 5 +++-- internal/controller/apibinding_controller.go | 5 +++-- internal/controller/store_controller.go | 5 +++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/cmd/model_generator.go b/cmd/model_generator.go index cfb2a948..be0f456b 100644 --- a/cmd/model_generator.go +++ b/cmd/model_generator.go @@ -84,7 +84,7 @@ var modelGeneratorCmd = &cobra.Command{ return err } - if err := controller.NewAPIBindingReconciler(log, mgr). + if err := controller.NewAPIBindingReconciler(ctx, log, mgr). SetupWithManager(mgr, defaultCfg); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Resource") return err diff --git a/cmd/operator.go b/cmd/operator.go index 395a62c1..3a6bdf55 100644 --- a/cmd/operator.go +++ b/cmd/operator.go @@ -12,6 +12,7 @@ import ( "github.com/platform-mesh/golang-commons/logger" "github.com/platform-mesh/golang-commons/sentry" 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/controller" "github.com/spf13/cobra" "google.golang.org/grpc" @@ -154,7 +155,7 @@ var operatorCmd = &cobra.Command{ fga := openfgav1.NewOpenFGAServiceClient(conn) - if err = controller.NewStoreReconciler(log, fga, mgr). + if err = controller.NewStoreReconciler(ctx, log, fga, mgr). SetupWithManager(mgr, defaultCfg); err != nil { log.Error().Err(err).Str("controller", "store").Msg("unable to create controller") return err @@ -199,7 +200,7 @@ var operatorCmd = &cobra.Command{ // this function can be removed after the operator has migrated the authz models in all environments func migrateAuthorizationModels(ctx context.Context, config *rest.Config, scheme *runtime.Scheme, getClusterClient NewLogicalClusterClientFunc) error { - allClient, err := controller.GetAllClient(config, scheme) + allClient, err := iclient.NewForAllPlatformMeshResources(ctx, config, scheme) if err != nil { return fmt.Errorf("failed to create all-cluster client: %w", err) } diff --git a/internal/controller/apibinding_controller.go b/internal/controller/apibinding_controller.go index bd10e729..7bfdb353 100644 --- a/internal/controller/apibinding_controller.go +++ b/internal/controller/apibinding_controller.go @@ -9,6 +9,7 @@ import ( "github.com/platform-mesh/golang-commons/controller/lifecycle/multicluster" lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/logger" + iclient "github.com/platform-mesh/security-operator/internal/client" "github.com/platform-mesh/security-operator/internal/subroutine" "github.com/rs/zerolog/log" ctrl "sigs.k8s.io/controller-runtime" @@ -76,8 +77,8 @@ func GetAllClient(config *rest.Config, schema *runtime.Scheme) (client.Client, e return allClient, nil } -func NewAPIBindingReconciler(logger *logger.Logger, mcMgr mcmanager.Manager) *APIBindingReconciler { - allclient, err := GetAllClient(mcMgr.GetLocalManager().GetConfig(), mcMgr.GetLocalManager().GetScheme()) +func NewAPIBindingReconciler(ctx context.Context, logger *logger.Logger, mcMgr mcmanager.Manager) *APIBindingReconciler { + allclient, err := iclient.NewForAllPlatformMeshResources(ctx, mcMgr.GetLocalManager().GetConfig(), mcMgr.GetLocalManager().GetScheme()) if err != nil { log.Fatal().Err(err).Msg("unable to create new client") } diff --git a/internal/controller/store_controller.go b/internal/controller/store_controller.go index 912ff60c..f0daa877 100644 --- a/internal/controller/store_controller.go +++ b/internal/controller/store_controller.go @@ -10,6 +10,7 @@ import ( lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "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" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -35,8 +36,8 @@ type StoreReconciler struct { mclifecycle *multicluster.LifecycleManager } -func NewStoreReconciler(log *logger.Logger, fga openfgav1.OpenFGAServiceClient, mcMgr mcmanager.Manager) *StoreReconciler { - allClient, err := GetAllClient(mcMgr.GetLocalManager().GetConfig(), mcMgr.GetLocalManager().GetScheme()) +func NewStoreReconciler(ctx context.Context, log *logger.Logger, fga openfgav1.OpenFGAServiceClient, mcMgr mcmanager.Manager) *StoreReconciler { + allClient, err := iclient.NewForAllPlatformMeshResources(ctx, mcMgr.GetLocalManager().GetConfig(), mcMgr.GetLocalManager().GetScheme()) if err != nil { log.Fatal().Err(err).Msg("unable to create new client") } From a87d1e5301242f88cf7d8ad56142cf72a1efb477 Mon Sep 17 00:00:00 2001 From: Simon Tesar Date: Thu, 19 Feb 2026 06:44:36 +0000 Subject: [PATCH 61/61] fix: manage finalizer on AccountInfo in initalizer and terminator to avoid race On-behalf-of: @SAP --- internal/subroutine/account_tuples.go | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/internal/subroutine/account_tuples.go b/internal/subroutine/account_tuples.go index 0324bbec..5bf2c95d 100644 --- a/internal/subroutine/account_tuples.go +++ b/internal/subroutine/account_tuples.go @@ -15,6 +15,7 @@ import ( kerrors "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" @@ -24,6 +25,8 @@ import ( kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" ) +const accountTuplesTerminatorFinalizer = "core.platform-mesh.io/account-tuples-terminator" + // AccountTuplesSubroutine creates FGA tuples for Accounts not of the // "org"-type when initializing, and deletes them when terminating. type AccountTuplesSubroutine struct { @@ -50,6 +53,22 @@ func (s *AccountTuplesSubroutine) Initialize(ctx context.Context, instance runti return ctrl.Result{}, opErr } + if updated := controllerutil.AddFinalizer(&ai, accountTuplesTerminatorFinalizer); updated { + lcID, ok := mccontext.ClusterFrom(ctx) + if !ok { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("cluster name not found in context"), true, true) + } + + lcClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(lcID)) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting client: %w", err), true, true) + } + + if err := lcClient.Update(ctx, &ai); err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("updating AccountInfo to set finalizer: %w", err), true, true) + } + } + // Ensure the necessary tuples in OpenFGA. tuples, err := fga.TuplesForAccount(acc, ai, s.creatorRelation, s.parentRelation, s.objectType) if err != nil { @@ -79,6 +98,22 @@ func (s *AccountTuplesSubroutine) Terminate(ctx context.Context, instance runtim return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("deleting tuples for Account: %w", err), true, true) } + if updated := controllerutil.RemoveFinalizer(&ai, accountTuplesTerminatorFinalizer); updated { + lcID, ok := mccontext.ClusterFrom(ctx) + if !ok { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("cluster name not found in context"), true, true) + } + + lcClient, err := iclient.NewForLogicalCluster(s.mgr.GetLocalManager().GetConfig(), s.mgr.GetLocalManager().GetScheme(), logicalcluster.Name(lcID)) + if err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("getting client: %w", err), true, true) + } + + if err := lcClient.Update(ctx, &ai); err != nil { + return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("updating AccountInfo to remove finalizer: %w", err), true, true) + } + } + return ctrl.Result{}, nil }