diff --git a/Dockerfile b/Dockerfile index c6e92b2..dc16bc2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ COPY go.sum go.sum RUN go mod download # Copy the go source -COPY cmd/main.go cmd/main.go +COPY cmd/ cmd/ COPY api/ api/ COPY internal/ internal/ @@ -21,7 +21,7 @@ COPY internal/ internal/ # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager ./cmd # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details diff --git a/Makefile b/Makefile index 425bae2..73e3104 100644 --- a/Makefile +++ b/Makefile @@ -202,7 +202,7 @@ lint-config: golangci-lint ## Verify golangci-lint linter configuration .PHONY: build build: manifests generate fmt vet ## Build manager binary. - go build -o bin/manager cmd/main.go + go build -o bin/manager ./cmd .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. diff --git a/chart/templates/deployment-operator-controller-manager.yaml b/chart/templates/deployment-operator-controller-manager.yaml index b83b91c..e3d51af 100644 --- a/chart/templates/deployment-operator-controller-manager.yaml +++ b/chart/templates/deployment-operator-controller-manager.yaml @@ -31,6 +31,9 @@ spec: - --leader-elect - --health-probe-bind-address=:8081 - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs + {{- if and .Values.controllers.enabled (not (has "*" .Values.controllers.enabled)) }} + - --controllers={{ join "," .Values.controllers.enabled }} + {{- end }} {{- if .Values.redirect.ingressClass }} - --redirect-ingress-class={{ .Values.redirect.ingressClass }} - --redirect-cluster-issuer={{ .Values.redirect.clusterIssuer.name }} diff --git a/chart/values.yaml b/chart/values.yaml index 430556f..5cfad51 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -97,6 +97,13 @@ podAnnotations: {} # Pod labels podLabels: {} +# Controllers to enable at startup. +# Use ["*"] to enable all (default — preserves existing behavior). +# Valid values: namespace, decofile, deco, decoredirect, operator-api +controllers: + enabled: + - "*" + # Cloudflare Workers build support cfworkers: existingSecret: "" # Secret with cf-api-token, cf-account-id diff --git a/cmd/controllers.go b/cmd/controllers.go new file mode 100644 index 0000000..f9f1a32 --- /dev/null +++ b/cmd/controllers.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "slices" + "strings" + + "github.com/deco-sites/decofile-operator/internal/api" + "github.com/deco-sites/decofile-operator/internal/controller" +) + +var knownControllers = []string{ + controller.TenantControllerName, + controller.DecofileControllerName, + controller.DecoControllerName, + controller.DecoRedirectControllerName, + api.ControllerName, +} + +// parseControllers parses a comma-separated list of controller names. +// "*" enables all known controllers. +// Returns an error if any name is not in knownControllers. +func parseControllers(input string) (func(string) bool, error) { + if strings.TrimSpace(input) == "*" { + return func(string) bool { return true }, nil + } + parts := strings.Split(input, ",") + set := make(map[string]bool, len(parts)) + for _, name := range parts { + name = strings.TrimSpace(name) + if name == "" { + return nil, fmt.Errorf("controller name must not be empty; valid values: %s", + strings.Join(knownControllers, ", ")) + } + if !slices.Contains(knownControllers, name) { + return nil, fmt.Errorf("unknown controller %q; valid values: %s", + name, strings.Join(knownControllers, ", ")) + } + set[name] = true + } + return func(name string) bool { return set[name] }, nil +} diff --git a/cmd/controllers_test.go b/cmd/controllers_test.go new file mode 100644 index 0000000..6b1b545 --- /dev/null +++ b/cmd/controllers_test.go @@ -0,0 +1,66 @@ +package main + +import ( + "strings" + "testing" + + "github.com/deco-sites/decofile-operator/internal/api" + "github.com/deco-sites/decofile-operator/internal/controller" +) + +func TestParseControllers_Star(t *testing.T) { + enabled, err := parseControllers("*") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + for _, name := range knownControllers { + if !enabled(name) { + t.Errorf("expected %q to be enabled with *, but it wasn't", name) + } + } +} + +func TestParseControllers_Subset(t *testing.T) { + enabled, err := parseControllers("decoredirect,operator-api") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !enabled(controller.DecoRedirectControllerName) { + t.Error("expected decoredirect to be enabled") + } + if !enabled(api.ControllerName) { + t.Error("expected operator-api to be enabled") + } + if enabled(controller.DecofileControllerName) { + t.Error("expected decofile to be disabled") + } + if enabled(controller.TenantControllerName) { + t.Error("expected namespace to be disabled") + } + if enabled(controller.DecoControllerName) { + t.Error("expected deco to be disabled") + } +} + +func TestParseControllers_UnknownName(t *testing.T) { + _, err := parseControllers("decoredirect,xpto") + if err == nil { + t.Fatal("expected error for unknown controller name, got nil") + } + if !strings.Contains(err.Error(), "xpto") { + t.Errorf("expected error to mention 'xpto', got: %v", err) + } +} + +func TestParseControllers_WhitespaceHandled(t *testing.T) { + enabled, err := parseControllers("decoredirect, operator-api") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !enabled(controller.DecoRedirectControllerName) { + t.Error("expected decoredirect to be enabled") + } + if !enabled(api.ControllerName) { + t.Error("expected operator-api to be enabled") + } +} diff --git a/cmd/main.go b/cmd/main.go index 06e122b..a937366 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -135,6 +135,10 @@ func main() { flag.StringVar(&redirectClusterIssuer, "redirect-cluster-issuer", getEnvOrDefault("REDIRECT_CLUSTER_ISSUER", "letsencrypt"), "cert-manager ClusterIssuer name (matches redirect.clusterIssuer.name in values).") + var controllersFlag string + flag.StringVar(&controllersFlag, "controllers", "*", + "Comma-separated list of controllers to enable. Use \"*\" to enable all. Valid values: "+ + strings.Join(knownControllers, ", ")) opts := zap.Options{ Development: false, } @@ -143,6 +147,12 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + enabled, err := parseControllers(controllersFlag) + if err != nil { + setupLog.Error(err, "invalid --controllers flag") + os.Exit(1) + } + // if the enable-http2 flag is false (the default), http/2 should be disabled // due to its vulnerabilities. More specifically, disabling http/2 will // prevent from being vulnerable to the HTTP/2 Stream Cancellation and @@ -256,134 +266,133 @@ func main() { os.Exit(1) } - // Build Valkey client. Falls back to a no-op client when Sentinel URLs are not configured - // so the operator works in environments where Valkey ACL provisioning is not needed. - var valkeyClient valkey.Client - switch { - case valkeyURL != "": - valkeyClient = valkey.NewDirectClient(valkeyURL, valkeyAdminPassword) - defer func() { _ = valkeyClient.Close() }() - setupLog.Info("Valkey ACL provisioning enabled (direct)", "addr", valkeyURL) - case valkeySentinelURLs != "": - valkeyClient = valkey.NewSentinelClient(valkey.Config{ - SentinelAddrs: strings.Split(valkeySentinelURLs, ","), - MasterName: valkeySentinelMaster, - AdminPassword: valkeyAdminPassword, - }) - defer func() { _ = valkeyClient.Close() }() - setupLog.Info("Valkey ACL provisioning enabled (sentinel)", - "sentinel", valkeySentinelURLs, "master", valkeySentinelMaster) - default: - valkeyClient = valkey.NoopClient{} - setupLog.Info("Valkey ACL provisioning disabled (set VALKEY_URL or VALKEY_SENTINEL_URLS)") - } - - nsReconciler := &controller.NamespaceReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - ValkeyClient: valkeyClient, - ResyncPeriod: valkeyResyncPeriod, - } - if err := nsReconciler.SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Namespace") - os.Exit(1) - } - // Start Sentinel failover watcher if enabled and Sentinel is configured. - // leaderElectedRunnable ensures only the active leader subscribes — prevents - // redundant TriggerResyncAll calls from non-leader replicas. - // Fail-safe: if subscription fails, operator continues with periodic resync. - if valkeyWatchFailover && valkeySentinelURLs != "" { - if err := mgr.Add(&leaderElectedRunnable{fn: func(ctx context.Context) error { - return valkeyClient.WatchFailover(ctx, func() { - controller.RecordSentinelFailover() - nsReconciler.TriggerResyncAll(ctx) + if enabled(controller.TenantControllerName) { + var valkeyClient valkey.Client + switch { + case valkeyURL != "": + valkeyClient = valkey.NewDirectClient(valkeyURL, valkeyAdminPassword) + defer func() { _ = valkeyClient.Close() }() + setupLog.Info("Valkey ACL provisioning enabled (direct)", "addr", valkeyURL) + case valkeySentinelURLs != "": + valkeyClient = valkey.NewSentinelClient(valkey.Config{ + SentinelAddrs: strings.Split(valkeySentinelURLs, ","), + MasterName: valkeySentinelMaster, + AdminPassword: valkeyAdminPassword, }) - }}); err != nil { - setupLog.Error(err, "unable to add Sentinel failover watcher (non-fatal)") - } else { - setupLog.Info("Sentinel failover watcher enabled") + defer func() { _ = valkeyClient.Close() }() + setupLog.Info("Valkey ACL provisioning enabled (sentinel)", + "sentinel", valkeySentinelURLs, "master", valkeySentinelMaster) + default: + valkeyClient = valkey.NoopClient{} + setupLog.Info("Valkey ACL provisioning disabled (set VALKEY_URL or VALKEY_SENTINEL_URLS)") } - // Watch for replica restarts (+reboot/-sdown) to re-provision only the - // affected node immediately, without waiting for the periodic resync cycle. - if err := mgr.Add(&leaderElectedRunnable{fn: func(ctx context.Context) error { - return valkeyClient.WatchNodeRestart(ctx, func(addr string) { - nsReconciler.ProvisionSingleNode(ctx, addr) - }) - }}); err != nil { - setupLog.Error(err, "unable to add node-restart watcher (non-fatal)") - } else { - setupLog.Info("Sentinel node-restart watcher enabled") + tenantReconciler := &controller.TenantReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ValkeyClient: valkeyClient, + ResyncPeriod: valkeyResyncPeriod, + } + if err = tenantReconciler.SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Namespace") + os.Exit(1) + } + if valkeyWatchFailover && valkeySentinelURLs != "" { + if err = mgr.Add(&leaderElectedRunnable{fn: func(ctx context.Context) error { + return valkeyClient.WatchFailover(ctx, func() { + controller.RecordSentinelFailover() + tenantReconciler.TriggerResyncAll(ctx) + }) + }}); err != nil { + setupLog.Error(err, "unable to add Sentinel failover watcher (non-fatal)") + } else { + setupLog.Info("Sentinel failover watcher enabled") + } + if err = mgr.Add(&leaderElectedRunnable{fn: func(ctx context.Context) error { + return valkeyClient.WatchNodeRestart(ctx, func(addr string) { + tenantReconciler.ProvisionSingleNode(ctx, addr) + }) + }}); err != nil { + setupLog.Error(err, "unable to add node-restart watcher (non-fatal)") + } else { + setupLog.Info("Sentinel node-restart watcher enabled") + } + } + if err = mgr.Add(manager.RunnableFunc(func(ctx context.Context) error { + if !mgr.GetCache().WaitForCacheSync(ctx) { + return fmt.Errorf("cache never synced") + } + return tenantReconciler.InitMetrics(ctx) + })); err != nil { + setupLog.Error(err, "unable to add metrics init runnable") + os.Exit(1) } } - // Seed the tenants_provisioned gauge from current cluster state once the cache is warm. - if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error { - if !mgr.GetCache().WaitForCacheSync(ctx) { - return fmt.Errorf("cache never synced") + if enabled(controller.DecofileControllerName) { + httpClient := controller.NewHTTPClient() + if err = (&controller.DecofileReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + HTTPClient: httpClient, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Decofile") + os.Exit(1) + } + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = webhookv1.SetupServiceWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Service") + os.Exit(1) + } + if err = webhookv1.SetupDecofileWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Decofile") + os.Exit(1) + } } - return nsReconciler.InitMetrics(ctx) - })); err != nil { - setupLog.Error(err, "unable to add metrics init runnable") - os.Exit(1) } - // Create shared HTTP client for pod notifications to prevent connection leaks - httpClient := controller.NewHTTPClient() - - if err := (&controller.DecofileReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - HTTPClient: httpClient, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Decofile") - os.Exit(1) - } - // nolint:goconst - if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err := webhookv1.SetupServiceWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Service") - os.Exit(1) + if enabled(controller.DecoControllerName) { + registry := build.NewBuilderRegistry() + registry.Register("cloudflare-worker", build.NewCloudflareFactory(build.CfWorkersConfigFromEnv())) + builderSAAnnotations := map[string]string{} + if roleArn := os.Getenv("BUILD_ROLE_ARN"); roleArn != "" { + builderSAAnnotations["eks.amazonaws.com/role-arn"] = roleArn } - if err := webhookv1.SetupDecofileWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Decofile") + if err = (&controller.DecoReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Builder: registry, + BuilderSAAnnotations: builderSAAnnotations, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Deco") os.Exit(1) } } - registry := build.NewBuilderRegistry() - registry.Register("cloudflare-worker", build.NewCloudflareFactory(build.CfWorkersConfigFromEnv())) - builderSAAnnotations := map[string]string{} - if roleArn := os.Getenv("BUILD_ROLE_ARN"); roleArn != "" { - builderSAAnnotations["eks.amazonaws.com/role-arn"] = roleArn - } - if err := (&controller.DecoReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Builder: registry, - BuilderSAAnnotations: builderSAAnnotations, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Deco") - os.Exit(1) - } - if err := (&controller.DecoRedirectReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - IngressClass: redirectIngressClass, - ClusterIssuer: redirectClusterIssuer, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "DecoRedirect") - os.Exit(1) - } - apiUser := os.Getenv("OPERATOR_API_USER") - apiPass := os.Getenv("OPERATOR_API_PASSWORD") - if apiUser != "" && apiPass != "" { - h := api.NewHandlers(mgr.GetClient(), redirectNamespace) - if err := mgr.Add(api.NewServer(operatorAPIAddr, apiUser, apiPass, h)); err != nil { - setupLog.Error(err, "unable to add operator API server") + if enabled(controller.DecoRedirectControllerName) { + if err = (&controller.DecoRedirectReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + IngressClass: redirectIngressClass, + ClusterIssuer: redirectClusterIssuer, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "DecoRedirect") os.Exit(1) } - setupLog.Info("Operator API enabled", "addr", operatorAPIAddr) + } + + if enabled(api.ControllerName) { + apiUser := os.Getenv("OPERATOR_API_USER") + apiPass := os.Getenv("OPERATOR_API_PASSWORD") + if apiUser != "" && apiPass != "" { + h := api.NewHandlers(mgr.GetClient(), redirectNamespace) + if err = mgr.Add(api.NewServer(operatorAPIAddr, apiUser, apiPass, h)); err != nil { + setupLog.Error(err, "unable to add operator API server") + os.Exit(1) + } + setupLog.Info("Operator API enabled", "addr", operatorAPIAddr) + } } // +kubebuilder:scaffold:builder diff --git a/hack/helm-generator/main.go b/hack/helm-generator/main.go index 97b4bff..c97b0d5 100644 --- a/hack/helm-generator/main.go +++ b/hack/helm-generator/main.go @@ -117,6 +117,9 @@ func main() { if err := addOperatorAPIIngress(templatesDir); err != nil { fmt.Fprintf(os.Stderr, "Warning: Could not add redirect API ingress: %v\n", err) } + if err := addControllersArg(templatesDir); err != nil { + fmt.Fprintf(os.Stderr, "Warning: Could not add controllers arg: %v\n", err) + } fmt.Printf("✓ Generated %d Helm templates\n\n", fileCount) fmt.Println("Test with:") @@ -424,6 +427,27 @@ func addRedirectControllerArgs(templatesDir string) error { return os.WriteFile(deploymentFile, []byte(contentStr), 0644) } +func addControllersArg(templatesDir string) error { + files, err := filepath.Glob(filepath.Join(templatesDir, "deployment-*.yaml")) + if err != nil || len(files) == 0 { + return fmt.Errorf("no deployment file found") + } + deploymentFile := files[0] + content, err := os.ReadFile(deploymentFile) + if err != nil { + return err + } + arg := ` {{- if and .Values.controllers.enabled (not (has "*" .Values.controllers.enabled)) }} + - --controllers={{ join "," .Values.controllers.enabled }} + {{- end }}` + anchor := ` - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs` + if !strings.Contains(string(content), anchor) { + return fmt.Errorf("anchor %q not found in %s", anchor, deploymentFile) + } + contentStr := strings.Replace(string(content), anchor, anchor+"\n"+arg, 1) + return os.WriteFile(deploymentFile, []byte(contentStr), 0644) +} + func addOperatorAPIEnvVars(templatesDir string) error { files, err := filepath.Glob(filepath.Join(templatesDir, "deployment-*.yaml")) if err != nil || len(files) == 0 { diff --git a/internal/api/server.go b/internal/api/server.go index 5ef5cdf..05ba842 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -7,6 +7,8 @@ import ( "time" ) +const ControllerName = "operator-api" + // Server exposes a minimal HTTP API for managing operator resources. // It implements manager.Runnable so controller-runtime starts it alongside controllers. // TLS is terminated at the ingress/NLB layer; the server listens on plain HTTP. diff --git a/internal/controller/deco_controller.go b/internal/controller/deco_controller.go index 7a825fa..8bcb983 100644 --- a/internal/controller/deco_controller.go +++ b/internal/controller/deco_controller.go @@ -22,9 +22,10 @@ import ( ) const ( - phaseRunning = "Running" - phaseSucceeded = "Succeeded" - phaseFailed = "Failed" + phaseRunning = "Running" + phaseSucceeded = "Succeeded" + phaseFailed = "Failed" + DecoControllerName = "deco" ) // DecoReconciler reconciles Deco objects. diff --git a/internal/controller/decofile_controller.go b/internal/controller/decofile_controller.go index ca7162d..ae73cb3 100644 --- a/internal/controller/decofile_controller.go +++ b/internal/controller/decofile_controller.go @@ -44,7 +44,10 @@ import ( decositesv1alpha1 "github.com/deco-sites/decofile-operator/api/v1alpha1" ) -const condTypePodsNotified = "PodsNotified" +const ( + condTypePodsNotified = "PodsNotified" + DecofileControllerName = "decofile" +) // deploymentIdLabel is declared in notifier.go (same package). diff --git a/internal/controller/decoredirect_controller.go b/internal/controller/decoredirect_controller.go index d6268d6..dfc8448 100644 --- a/internal/controller/decoredirect_controller.go +++ b/internal/controller/decoredirect_controller.go @@ -34,6 +34,7 @@ type DecoRedirectReconciler struct { // dummyBackendName satisfies the k8s Ingress API requirement for a backend on every path. // nginx never routes to it because permanent-redirect intercepts first. const dummyBackendName = "redirect-dummy-backend" +const DecoRedirectControllerName = "decoredirect" // +kubebuilder:rbac:groups=deco.sites,resources=decoredict,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=deco.sites,resources=decoredict/status,verbs=get;update;patch diff --git a/internal/controller/namespace_controller.go b/internal/controller/namespace_controller.go index 48a52af..de86c1d 100644 --- a/internal/controller/namespace_controller.go +++ b/internal/controller/namespace_controller.go @@ -46,6 +46,7 @@ const ( valkeySecretName = "valkey-acl" siteNamespacePrefix = "sites-" valkeyReservedDefault = "default" + TenantControllerName = "tenant" ) // +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch;update;patch @@ -60,7 +61,7 @@ const DefaultResyncPeriod = 10 * time.Minute // valkeyACLAnnotationValue is the expected value of the opt-in annotation. const valkeyACLAnnotationValue = "true" -// NamespaceReconciler provisions per-tenant Valkey ACL credentials for site namespaces. +// TenantReconciler provisions per-tenant Valkey ACL credentials for site namespaces. // When a Namespace has the annotation "deco.sites/valkey-acl: true", the reconciler: // - Creates a Valkey ACL user restricted to the site's key prefix. // - Creates a K8s Secret "valkey-acl" in that namespace with the credentials. @@ -94,7 +95,7 @@ const valkeyACLAnnotationValue = "true" // // TODO: when enabling auth, extend ValkeyClient to provision ACL SETUSER on // all nodes (master + every replica), not only the Sentinel master. -type NamespaceReconciler struct { +type TenantReconciler struct { client.Client Scheme *runtime.Scheme ValkeyClient valkey.Client @@ -104,7 +105,7 @@ type NamespaceReconciler struct { // ProvisionSingleNode re-provisions all managed namespaces on one specific Valkey // node. Called when Sentinel detects a replica restart (+reboot/-sdown events) so // only the restarted node is updated — no unnecessary work on healthy nodes. -func (r *NamespaceReconciler) ProvisionSingleNode(ctx context.Context, nodeAddr string) { +func (r *TenantReconciler) ProvisionSingleNode(ctx context.Context, nodeAddr string) { log := logf.FromContext(ctx).WithName("valkey-node-provision").WithValues("node", nodeAddr) nsList := &corev1.NamespaceList{} if err := r.List(ctx, nsList); err != nil { @@ -141,7 +142,7 @@ func (r *NamespaceReconciler) ProvisionSingleNode(ctx context.Context, nodeAddr // TriggerResyncAll immediately re-queues all managed namespaces by updating a // sync annotation. Called on Sentinel failover events to recover ACLs without // waiting for the next periodic resync cycle. -func (r *NamespaceReconciler) TriggerResyncAll(ctx context.Context) { +func (r *TenantReconciler) TriggerResyncAll(ctx context.Context) { log := logf.FromContext(ctx).WithName("valkey-resync") nsList := &corev1.NamespaceList{} if err := r.List(ctx, nsList); err != nil { @@ -171,7 +172,7 @@ func (r *NamespaceReconciler) TriggerResyncAll(ctx context.Context) { // InitMetrics seeds the tenants_provisioned gauge from current cluster state. // Must be called after the cache is synced (i.e. inside a Runnable or after mgr.Start). -func (r *NamespaceReconciler) InitMetrics(ctx context.Context) error { +func (r *TenantReconciler) InitMetrics(ctx context.Context) error { nsList := &corev1.NamespaceList{} if err := r.List(ctx, nsList); err != nil { return err @@ -196,7 +197,7 @@ func (r *NamespaceReconciler) InitMetrics(ctx context.Context) error { // SetupWithManager registers the Namespace controller with a resync period for // self-healing (recovers ACLs lost after a Valkey restart). -func (r *NamespaceReconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *TenantReconciler) SetupWithManager(mgr ctrl.Manager) error { // Watch Secrets named "valkey-acl" and enqueue the parent Namespace. // Namespace is cluster-scoped so Owns() (which relies on owner references) cannot // be used across scopes. Instead we map Secret → Namespace by name. @@ -221,7 +222,7 @@ func (r *NamespaceReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *NamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +func (r *TenantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := logf.FromContext(ctx).WithValues("namespace", req.Name) ns := &corev1.Namespace{} @@ -339,7 +340,7 @@ func (r *NamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( // createSecret creates the "valkey-acl" Secret in the given namespace with // credentials ready to be consumed by deco via LOADER_CACHE_REDIS_USERNAME/PASSWORD. -func (r *NamespaceReconciler) createSecret(ctx context.Context, namespace, username, password string) error { +func (r *TenantReconciler) createSecret(ctx context.Context, namespace, username, password string) error { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: valkeySecretName,