diff --git a/cmd/main.go b/cmd/main.go index 380ff53ac0..7e41b7d24b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -596,6 +596,7 @@ admission policy installation; once an Installation exists it is the authority o UseV3CRDs: v3CRDs, APIDiscovery: apiDiscovery, Extensions: extensionRegistry, + Controllers: enterprise.Controllers(variant), } err = controller.AddToManager(mgr, options) diff --git a/docs/principles.md b/docs/principles.md index 28881a941d..96e090c9e8 100644 --- a/docs/principles.md +++ b/docs/principles.md @@ -34,6 +34,7 @@ API design principles and the Go/kubebuilder coding conventions for `api/v1` CRD ## Variants - **Core code is variant-blind.** Controllers and render packages outside `pkg/enterprise` must not name a variant, in code or in comments. Behavior a single variant needs registers through `pkg/extensions`. +- **A controller only one variant runs lives in `pkg/enterprise/controller`, and its render code in `pkg/enterprise/render`.** Both mirror the core tree they came from. The controller is contributed through the controller list on `ControllerOptions` rather than named by `AddToManager`, so it carries no variant check of its own. ## Security diff --git a/internal/controller/controllers.go b/internal/controller/controllers.go index 2318499430..9b245a0f3a 100644 --- a/internal/controller/controllers.go +++ b/internal/controller/controllers.go @@ -80,13 +80,6 @@ func AddToManager(mgr ctrl.Manager, options options.ControllerOptions) error { }).SetupWithManager(mgr, options); err != nil { return fmt.Errorf("failed to create controller %s: %v", "ApplicationLayer", err) } - if err := (&MonitorReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("Monitor"), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr, options); err != nil { - return fmt.Errorf("failed to create controller %s: %v", "Monitor", err) - } if err := (&ManagerReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("Manager"), @@ -202,5 +195,8 @@ func AddToManager(mgr ctrl.Manager, options options.ControllerOptions) error { return fmt.Errorf("failed to create controller %s: %v", "OpenTelemetry", err) } // +kubebuilder:scaffold:builder - return nil + + // The controllers only the running variant supplies, added last so that a variant + // can watch resources the core controllers own. + return options.AddControllers(mgr) } diff --git a/internal/controller/monitor_controller.go b/internal/controller/monitor_controller.go deleted file mode 100644 index 4f7f6458c3..0000000000 --- a/internal/controller/monitor_controller.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2021-2026 Tigera, Inc. All rights reserved. -/* - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controller - -import ( - "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/tigera/operator/pkg/controller/monitor" - "github.com/tigera/operator/pkg/controller/options" -) - -// MonitorReconciler reconciles a Monitor object -type MonitorReconciler struct { - client.Client - Log logr.Logger - Scheme *runtime.Scheme -} - -// +kubebuilder:rbac:groups=operator.tigera.io,resources=monitors,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=operator.tigera.io,resources=monitors/status,verbs=get;update;patch - -func (r *MonitorReconciler) SetupWithManager(mgr ctrl.Manager, opts options.ControllerOptions) error { - return monitor.Add(mgr, opts) -} diff --git a/pkg/controller/options/options.go b/pkg/controller/options/options.go index 5bd003a1e6..55a973e249 100644 --- a/pkg/controller/options/options.go +++ b/pkg/controller/options/options.go @@ -16,6 +16,9 @@ package options import ( "context" + "fmt" + + ctrl "sigs.k8s.io/controller-runtime" v1 "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/common" @@ -77,4 +80,27 @@ type ControllerOptions struct { // Extensions are the variant extensions the operator runs with, for the Variant // above. The core operator leaves them unset and runs the base behavior. Extensions extensions.Extensions + + // Controllers are the reconcilers the running variant adds to the core set. The + // core operator leaves them unset and runs only the controllers every variant runs. + Controllers []Controller +} + +// AddControllers adds the reconcilers the running variant contributes. +func (o ControllerOptions) AddControllers(mgr ctrl.Manager) error { + for _, c := range o.Controllers { + if err := c.Add(mgr, o); err != nil { + return fmt.Errorf("failed to create controller %s: %v", c.Name, err) + } + } + return nil +} + +// Controller is a reconciler a variant contributes, so that the core controller +// manager can add it without naming the type. +type Controller struct { + // Name identifies the controller when its setup fails. + Name string + + Add func(mgr ctrl.Manager, opts ControllerOptions) error } diff --git a/pkg/controller/options/options_test.go b/pkg/controller/options/options_test.go new file mode 100644 index 0000000000..cd9b12d683 --- /dev/null +++ b/pkg/controller/options/options_test.go @@ -0,0 +1,75 @@ +// Copyright (c) 2026 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options_test + +import ( + "errors" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + ctrl "sigs.k8s.io/controller-runtime" + + operatorv1 "github.com/tigera/operator/api/v1" + "github.com/tigera/operator/pkg/controller/options" +) + +func TestOptions(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "pkg/controller/options Suite") +} + +var _ = Describe("AddControllers", func() { + It("adds every contributed controller, in order", func() { + var added []string + add := func(name string) options.Controller { + return options.Controller{Name: name, Add: func(ctrl.Manager, options.ControllerOptions) error { + added = append(added, name) + return nil + }} + } + opts := options.ControllerOptions{Controllers: []options.Controller{add("First"), add("Second")}} + + Expect(opts.AddControllers(nil)).NotTo(HaveOccurred()) + Expect(added).To(Equal([]string{"First", "Second"})) + }) + + It("passes the options through to the controller", func() { + var got options.ControllerOptions + opts := options.ControllerOptions{ + Variant: operatorv1.CalicoEnterprise, + Controllers: []options.Controller{{Name: "Monitor", Add: func(_ ctrl.Manager, o options.ControllerOptions) error { + got = o + return nil + }}}, + } + + Expect(opts.AddControllers(nil)).NotTo(HaveOccurred()) + Expect(got.Variant).To(Equal(operatorv1.CalicoEnterprise)) + }) + + It("names the controller that failed", func() { + opts := options.ControllerOptions{Controllers: []options.Controller{{ + Name: "Monitor", + Add: func(ctrl.Manager, options.ControllerOptions) error { return errors.New("no watch") }, + }}} + + Expect(opts.AddControllers(nil)).To(MatchError(ContainSubstring("controller Monitor: no watch"))) + }) + + It("does nothing when the variant contributes none", func() { + Expect(options.ControllerOptions{}.AddControllers(nil)).NotTo(HaveOccurred()) + }) +}) diff --git a/pkg/controller/monitor/alertmanager-config.yaml b/pkg/enterprise/controller/monitor/alertmanager-config.yaml similarity index 100% rename from pkg/controller/monitor/alertmanager-config.yaml rename to pkg/enterprise/controller/monitor/alertmanager-config.yaml diff --git a/pkg/controller/monitor/monitor_controller.go b/pkg/enterprise/controller/monitor/monitor_controller.go similarity index 99% rename from pkg/controller/monitor/monitor_controller.go rename to pkg/enterprise/controller/monitor/monitor_controller.go index 872cac913f..e29ef74e66 100644 --- a/pkg/controller/monitor/monitor_controller.go +++ b/pkg/enterprise/controller/monitor/monitor_controller.go @@ -61,10 +61,6 @@ const ResourceName = "monitor" var log = logf.Log.WithName("controller_monitor") func Add(mgr manager.Manager, opts options.ControllerOptions) error { - if !opts.Variant.IsEnterprise() { - return nil - } - prometheusReady := &utils.ReadyFlag{} tierWatchReady := &utils.ReadyFlag{} licenseAPIReady := &utils.ReadyFlag{} diff --git a/pkg/controller/monitor/monitor_controller_suite_test.go b/pkg/enterprise/controller/monitor/monitor_controller_suite_test.go similarity index 86% rename from pkg/controller/monitor/monitor_controller_suite_test.go rename to pkg/enterprise/controller/monitor/monitor_controller_suite_test.go index 1a3526bf70..ae94b2d5a6 100644 --- a/pkg/controller/monitor/monitor_controller_suite_test.go +++ b/pkg/enterprise/controller/monitor/monitor_controller_suite_test.go @@ -29,6 +29,6 @@ func TestStatus(t *testing.T) { logf.SetLogger(zap.New(zap.WriteTo(ginkgo.GinkgoWriter), zap.UseDevMode(true), zap.Level(uzap.NewAtomicLevelAt(uzap.DebugLevel)))) gomega.RegisterFailHandler(ginkgo.Fail) suiteConfig, reporterConfig := ginkgo.GinkgoConfiguration() - reporterConfig.JUnitReport = "../../../report/ut/monitor_controller_suite.xml" - ginkgo.RunSpecs(t, "pkg/controller/monitor Suite", suiteConfig, reporterConfig) + reporterConfig.JUnitReport = "../../../../report/ut/monitor_controller_suite.xml" + ginkgo.RunSpecs(t, "pkg/enterprise/controller/monitor Suite", suiteConfig, reporterConfig) } diff --git a/pkg/controller/monitor/monitor_controller_test.go b/pkg/enterprise/controller/monitor/monitor_controller_test.go similarity index 100% rename from pkg/controller/monitor/monitor_controller_test.go rename to pkg/enterprise/controller/monitor/monitor_controller_test.go diff --git a/pkg/controller/monitor/prometheus.go b/pkg/enterprise/controller/monitor/prometheus.go similarity index 100% rename from pkg/controller/monitor/prometheus.go rename to pkg/enterprise/controller/monitor/prometheus.go diff --git a/pkg/enterprise/controllers.go b/pkg/enterprise/controllers.go new file mode 100644 index 0000000000..efffe12500 --- /dev/null +++ b/pkg/enterprise/controllers.go @@ -0,0 +1,33 @@ +// Copyright (c) 2026 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package enterprise + +import ( + operatorv1 "github.com/tigera/operator/api/v1" + "github.com/tigera/operator/pkg/controller/options" + "github.com/tigera/operator/pkg/enterprise/controller/monitor" +) + +// Controllers returns the reconcilers only Calico Enterprise runs, for the caller to +// pass to the controller manager. Registering here is what gates them, so the +// controllers themselves do not check the variant. +func Controllers(variant operatorv1.ProductVariant) []options.Controller { + if !variant.IsEnterprise() { + return nil + } + return []options.Controller{ + {Name: "Monitor", Add: monitor.Add}, + } +} diff --git a/pkg/enterprise/controllers_test.go b/pkg/enterprise/controllers_test.go new file mode 100644 index 0000000000..c88ba6bb8d --- /dev/null +++ b/pkg/enterprise/controllers_test.go @@ -0,0 +1,43 @@ +// Copyright (c) 2026 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package enterprise_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + operatorv1 "github.com/tigera/operator/api/v1" + "github.com/tigera/operator/pkg/enterprise" +) + +var _ = Describe("Controllers", func() { + DescribeTable("contributes the Enterprise-only controllers", + func(variant operatorv1.ProductVariant) { + names := []string{} + for _, c := range enterprise.Controllers(variant) { + Expect(c.Add).NotTo(BeNil()) + names = append(names, c.Name) + } + Expect(names).To(ContainElement("Monitor")) + }, + Entry("CalicoEnterprise", operatorv1.CalicoEnterprise), + //nolint:staticcheck // SA1019: the deprecated spelling is what this covers + Entry("TigeraSecureEnterprise", operatorv1.TigeraSecureEnterprise), + ) + + It("contributes nothing for Calico", func() { + Expect(enterprise.Controllers(operatorv1.Calico)).To(BeEmpty()) + }) +}) diff --git a/pkg/enterprise/csr/extension.go b/pkg/enterprise/csr/extension.go index 0b9f4e88f5..7d496a956d 100644 --- a/pkg/enterprise/csr/extension.go +++ b/pkg/enterprise/csr/extension.go @@ -33,9 +33,9 @@ import ( operatorv1 "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/controller" - "github.com/tigera/operator/pkg/controller/monitor" "github.com/tigera/operator/pkg/controller/utils" "github.com/tigera/operator/pkg/ctrlruntime" + "github.com/tigera/operator/pkg/enterprise/controller/monitor" eutils "github.com/tigera/operator/pkg/enterprise/utils" "github.com/tigera/operator/pkg/extensions" "github.com/tigera/operator/pkg/render" diff --git a/pkg/enterprise/csr/extension_test.go b/pkg/enterprise/csr/extension_test.go index 43edb260ac..0d1565e74f 100644 --- a/pkg/enterprise/csr/extension_test.go +++ b/pkg/enterprise/csr/extension_test.go @@ -36,10 +36,10 @@ import ( operatorv1 "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/apis" "github.com/tigera/operator/pkg/controller" - "github.com/tigera/operator/pkg/controller/monitor" "github.com/tigera/operator/pkg/ctrlruntime" ctrlrfake "github.com/tigera/operator/pkg/ctrlruntime/client/fake" "github.com/tigera/operator/pkg/dns" + "github.com/tigera/operator/pkg/enterprise/controller/monitor" "github.com/tigera/operator/pkg/render" rmonitor "github.com/tigera/operator/pkg/render/monitor" ) diff --git a/test/mainline_test.go b/test/mainline_test.go index 5615847902..4b78aca90b 100644 --- a/test/mainline_test.go +++ b/test/mainline_test.go @@ -340,6 +340,7 @@ func setupManager(manageCRDs bool, multiTenant bool, variant operator.ProductVar DetectedProvider: operator.ProviderNone, Variant: variant, Extensions: enterprise.New(variant, eoptions.Options{}), + Controllers: enterprise.Controllers(variant), ManageCRDs: manageCRDs, ShutdownContext: ctx, K8sClientset: clientset,