diff --git a/Makefile b/Makefile index 4c28e8238d..081fcd8d25 100644 --- a/Makefile +++ b/Makefile @@ -169,10 +169,8 @@ EXCLUDE_MANIFEST_REGISTRIES?=gcr.io/ # amd64 only. Constrain ARCHES (not just VALIDARCHES) so push-all, which iterates ARCHES, does not # try to push arches that were never built. ARCHES:=amd64 -# Bake cloud mode into the operator binary so it cannot be disabled at runtime (see isCloudBuild in -# cmd/cloud.go). buildVariant lives in package main, which the linker addresses as "main" (not by its -# import path), so this -X target is "main.buildVariant" rather than a $(PACKAGE_NAME)-prefixed path. -CLOUD_LDFLAGS=-X main.buildVariant=cloud +# Bake cloud mode into the operator binary so it cannot be disabled at runtime. +CLOUD_LDFLAGS=-X $(PACKAGE_NAME)/version.BuildVariant=cloud endif BUILD_IMAGE?=tigera/operator diff --git a/cmd/main.go b/cmd/main.go index e52d1f8a21..380ff53ac0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -43,7 +43,6 @@ import ( "github.com/tigera/operator/pkg/controller/utils" "github.com/tigera/operator/pkg/dns" "github.com/tigera/operator/pkg/enterprise" - eoptions "github.com/tigera/operator/pkg/enterprise/options" "github.com/tigera/operator/pkg/imports/admission" "github.com/tigera/operator/pkg/imports/crds" "github.com/tigera/operator/pkg/render" @@ -87,14 +86,6 @@ var ( // configuration for the operator loaded at startup. const bootstrapConfigMapName = "operator-bootstrap-config" -// buildVariant is set to "cloud" via -ldflags "-X main.buildVariant=cloud" when building the Calico -// Cloud operator image (see CLOUD_LDFLAGS in the Makefile), and is empty otherwise. -var buildVariant string - -func isCloudBuild() bool { - return buildVariant == "cloud" -} - func init() { // +kubebuilder:scaffold:scheme utilruntime.Must(clientgoscheme.AddToScheme(scheme)) @@ -104,8 +95,8 @@ func init() { func printVersion() { log.Info(fmt.Sprintf("Version: %v", version.VERSION)) - if isCloudBuild() { - log.Info("Variant: Calico Cloud") + if version.BuildVariant != "" { + log.Info(fmt.Sprintf("Variant: %s", version.BuildVariant)) } log.Info(fmt.Sprintf("Go Version: %s", goruntime.Version())) log.Info(fmt.Sprintf("Go OS/Arch: %s/%s", goruntime.GOOS, goruntime.GOARCH)) @@ -191,8 +182,8 @@ admission policy installation; once an Installation exists it is the authority o fmt.Println("Operator:", version.VERSION) fmt.Println("Calico:", components.CalicoRelease) fmt.Println("Enterprise:", components.EnterpriseRelease) - if isCloudBuild() { - fmt.Println("Variant: Calico Cloud") + if version.BuildVariant != "" { + fmt.Println("Variant:", version.BuildVariant) } os.Exit(0) } @@ -425,6 +416,32 @@ admission policy installation; once an Installation exists it is the authority o } } + clientset, err := kubernetes.NewForConfig(mgr.GetConfig()) + if err != nil { + log.Error(err, "Failed to get Kubernetes clientset") + os.Exit(1) + } + + // Attempt to auto discover the provider + provider, err := discovery.AutoDiscoverProvider(ctx, clientset) + if err != nil { + setupLog.Error(err, "Auto discovery of Provider failed") + os.Exit(1) + } + setupLog.WithValues("provider", provider).Info("Checking type of cluster") + + clusterDomain, err := dns.GetClusterDomain(dns.DefaultResolveConfPath) + if err != nil { + clusterDomain = dns.DefaultClusterDomain + log.Error(err, fmt.Sprintf("Couldn't find the cluster domain from the resolv.conf, defaulting to %s", clusterDomain)) + } + + kubernetesVersion, err := common.GetKubernetesVersion(clientset) + if err != nil { + log.Error(err, "Unable to resolve Kubernetes version, defaulting to v1.18") + kubernetesVersion = &common.VersionInfo{Major: 1, Minor: 18} + } + // Resolve the variant now that the operator CRDs exist. variant := waitForVariant(ctx, c, setupLog) setupLog.WithValues("variant", variant).Info("Resolved product variant") @@ -439,9 +456,16 @@ admission policy installation; once an Installation exists it is the authority o } } + extensionRegistry, err := enterprise.Build(ctx, variant, clientset, manageCRDs, v3CRDs) + if err != nil { + setupLog.Error(err, "Failed to build the variant's extensions") + os.Exit(1) + } + setupLog.WithValues("tenancy", extensionRegistry.Startup().MultiTenant()).Info("Checking tenancy mode") + // The variant's controllers can't register without their APIs. Exiting lets the kubelet // retry us once the CRDs are installed. - if err := enterprise.VerifyAPIsExist(variant, cs); err != nil { + if err := extensionRegistry.Startup().VerifyAPIsExist(cs); err != nil { setupLog.Error(err, "Cannot run as the configured variant") os.Exit(1) } @@ -508,45 +532,11 @@ admission policy installation; once an Installation exists it is the authority o } }() - clientset, err := kubernetes.NewForConfig(mgr.GetConfig()) - if err != nil { - log.Error(err, "Failed to get Kubernetes clientset") - os.Exit(1) - } - - // Attempt to auto discover the provider - provider, err := discovery.AutoDiscoverProvider(ctx, clientset) - if err != nil { - setupLog.Error(err, "Auto discovery of Provider failed") - os.Exit(1) - } - setupLog.WithValues("provider", provider).Info("Checking type of cluster") - - // Determine if we're running in single or multi-tenant mode. - multiTenant, err := discovery.MultiTenant(ctx, clientset) - if err != nil { - log.Error(err, "Failed to discovery tenancy mode") - os.Exit(1) - } - setupLog.WithValues("tenancy", multiTenant).Info("Checking tenancy mode") - - clusterDomain, err := dns.GetClusterDomain(dns.DefaultResolveConfPath) - if err != nil { - clusterDomain = dns.DefaultClusterDomain - log.Error(err, fmt.Sprintf("Couldn't find the cluster domain from the resolv.conf, defaulting to %s", clusterDomain)) - } - - kubernetesVersion, err := common.GetKubernetesVersion(clientset) - if err != nil { - log.Error(err, "Unable to resolve Kubernetes version, defaulting to v1.18") - kubernetesVersion = &common.VersionInfo{Major: 1, Minor: 18} - } - // The operator MUST not run within one of the Namespaces that it itself manages. Perform an early check here // to make sure that we're not doing so, and exit if we are. // Components share namespaces, so dedupe before the error lists them. badNamespaces := sets.New(common.CalicoNamespace, render.CSIDaemonSetNamespace). - Insert(enterprise.ProtectedNamespaces()...). + Insert(extensionRegistry.Startup().ProtectedNamespaces()...). UnsortedList() for _, ns := range badNamespaces { if common.OperatorNamespace() == ns { @@ -569,11 +559,11 @@ admission policy installation; once an Installation exists it is the authority o useSingleIndex := false useExternalElastic := discovery.UseExternalElastic(bootConfig) - if isCloudBuild() { + if extensionRegistry.Startup().Cloud() { elasticIsMigrating = discovery.ElasticIsMigrating(bootConfig) useSingleIndex = discovery.UseSingleIndex(bootConfig) - if err := enterprise.VerifyElasticsearch(ctx, cs, variant, elasticIsMigrating, useExternalElastic); err != nil { - setupLog.Error(err, "Elasticsearch configuration verification failed") + if err := extensionRegistry.Startup().VerifyClusterState(ctx, cs, elasticIsMigrating, useExternalElastic); err != nil { + setupLog.Error(err, "Cluster state verification failed") os.Exit(1) } } @@ -590,14 +580,6 @@ admission policy installation; once an Installation exists it is the authority o os.Exit(1) } - // Build the extensions for the variant we resolved above. - extensionRegistry := enterprise.New(variant, eoptions.Options{ - MultiTenant: multiTenant, - Cloud: isCloudBuild(), - ManageCRDs: manageCRDs, - UseV3CRDs: v3CRDs, - }) - options := options.ControllerOptions{ DetectedProvider: provider, Variant: variant, @@ -606,9 +588,9 @@ admission policy installation; once an Installation exists it is the authority o ManageCRDs: manageCRDs, ShutdownContext: ctx, K8sClientset: clientset, - MultiTenant: multiTenant, + MultiTenant: extensionRegistry.Startup().MultiTenant(), ElasticExternal: useExternalElastic, - Cloud: isCloudBuild(), + Cloud: extensionRegistry.Startup().Cloud(), ESMigration: elasticIsMigrating, UseSingleIndex: useSingleIndex, UseV3CRDs: v3CRDs, diff --git a/pkg/enterprise/register.go b/pkg/enterprise/register.go index 4c4bd769f5..c7679cd3b9 100644 --- a/pkg/enterprise/register.go +++ b/pkg/enterprise/register.go @@ -15,7 +15,13 @@ package enterprise import ( + "context" + "fmt" + + "k8s.io/client-go/kubernetes" + operatorv1 "github.com/tigera/operator/api/v1" + "github.com/tigera/operator/pkg/common/discovery" "github.com/tigera/operator/pkg/components" "github.com/tigera/operator/pkg/enterprise/apiserver" "github.com/tigera/operator/pkg/enterprise/clusterconnection" @@ -29,39 +35,68 @@ import ( "github.com/tigera/operator/pkg/enterprise/whisker" "github.com/tigera/operator/pkg/enterprise/windows" "github.com/tigera/operator/pkg/extensions" + "github.com/tigera/operator/version" ) -// New builds the Calico Enterprise extensions. After the monorepo split this is what -// calico-private's main constructs instead. +// Build returns the Calico Enterprise extensions, and is called once the variant is +// resolved. Tenancy and the cloud build flag are resolved here rather than passed in, +// since each only ever describes an Enterprise install. +func Build(ctx context.Context, variant operatorv1.ProductVariant, clientset kubernetes.Interface, manageCRDs, useV3CRDs bool) (extensions.Extensions, error) { + o := eoptions.Options{ + ManageCRDs: manageCRDs, + UseV3CRDs: useV3CRDs, + Cloud: isCloudBuild(), + } + + if variant.IsEnterprise() { + // Tenancy shows up as a namespaced Manager, a CRD only Enterprise installs. + multiTenant, err := discovery.MultiTenant(ctx, clientset) + if err != nil { + return extensions.Extensions{}, fmt.Errorf("failed to determine the tenancy mode: %w", err) + } + o.MultiTenant = multiTenant + } + + return New(variant, o), nil +} + +// New builds the Calico Enterprise extensions from options already resolved. func New(variant operatorv1.ProductVariant, o eoptions.Options) extensions.Extensions { + // Startup is registered whatever the variant, since the namespaces Enterprise + // manages are off limits to a Calico install too. + set := extensions.Set{Startup: startup{variant: variant, opts: o}} + // Enterprise has two spellings, so match on the product rather than the constant: // an Installation asking for the deprecated TigeraSecureEnterprise still gets the // Enterprise extensions. - if variant.IsEnterprise() { + switch { + case variant.IsEnterprise(): // Registered here so the images arrive with the extensions. A test that wants // this build's own images instead calls components.UseImages. components.RegisterVariantImages(components.EnterpriseImages) - return extensions.New(extensions.Set{ - Installation: installation.New(variant, o), - Windows: windows.New(variant), - APIServer: apiserver.New(variant, o), - ClusterConnection: clusterconnection.New(variant), - Tiers: tiers.New(o), - CSR: csr.New(), - Istio: istio.New(variant), - Goldmane: goldmane.New(variant), - Whisker: whisker.New(variant), - GatewayAPI: gatewayapi.New(variant), - }) - } - - if variant == operatorv1.Calico { + set.Installation = installation.New(variant, o) + set.Windows = windows.New(variant) + set.APIServer = apiserver.New(variant, o) + set.ClusterConnection = clusterconnection.New(variant) + set.Tiers = tiers.New(o) + set.CSR = csr.New() + set.Istio = istio.New(variant) + set.Goldmane = goldmane.New(variant) + set.Whisker = whisker.New(variant) + set.GatewayAPI = gatewayapi.New(variant) + case variant == operatorv1.Calico: // Clean up what a prior Enterprise installation left behind. - return extensions.New(extensions.Set{APIServer: apiserver.CalicoCleanup{}}) + set.APIServer = apiserver.CalicoCleanup{} } - return extensions.Extensions{} + return extensions.New(set) +} + +// isCloudBuild reports whether the binary was built for Calico Cloud. The Makefile bakes +// the answer in through CLOUD_LDFLAGS so it cannot be flipped at runtime. +func isCloudBuild() bool { + return version.BuildVariant == "cloud" } // Images is the image set Enterprise runs, for the caller to register. New leaves it diff --git a/pkg/enterprise/startup.go b/pkg/enterprise/startup.go index 8c0bdcdd6c..f8404f8492 100644 --- a/pkg/enterprise/startup.go +++ b/pkg/enterprise/startup.go @@ -24,6 +24,7 @@ import ( operatorv1 "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/common/discovery" + eoptions "github.com/tigera/operator/pkg/enterprise/options" "github.com/tigera/operator/pkg/render" "github.com/tigera/operator/pkg/render/intrusiondetection/dpi" "github.com/tigera/operator/pkg/render/istio" @@ -31,9 +32,17 @@ import ( "github.com/tigera/operator/pkg/render/logstorage/eck" ) +// startup is the Enterprise hook into operator startup. It registers for every variant, +// since the namespaces Enterprise manages are off limits to a Calico install too, and +// no-ops the checks that only apply to Enterprise. +type startup struct { + variant operatorv1.ProductVariant + opts eoptions.Options +} + // VerifyAPIsExist reports whether the Enterprise CRDs the extension controllers need are installed. -func VerifyAPIsExist(variant operatorv1.ProductVariant, cs kubernetes.Interface) error { - if !variant.IsEnterprise() { +func (s startup) VerifyAPIsExist(cs kubernetes.Interface) error { + if !s.variant.IsEnterprise() { return nil } @@ -47,10 +56,10 @@ func VerifyAPIsExist(variant operatorv1.ProductVariant, cs kubernetes.Interface) return nil } -// VerifyElasticsearch rejects a cluster whose Elasticsearch certificates contradict the +// VerifyClusterState rejects a cluster whose Elasticsearch certificates contradict the // internal or external mode the operator is configured for. -func VerifyElasticsearch(ctx context.Context, cs kubernetes.Interface, variant operatorv1.ProductVariant, migrating, external bool) error { - if !variant.IsEnterprise() { +func (s startup) VerifyClusterState(ctx context.Context, cs kubernetes.Interface, migrating, external bool) error { + if !s.variant.IsEnterprise() { return nil } @@ -80,7 +89,7 @@ func VerifyElasticsearch(ctx context.Context, cs kubernetes.Interface, variant o // ProtectedNamespaces returns the Enterprise namespaces the operator manages and so // must not run in itself. -func ProtectedNamespaces() []string { +func (s startup) ProtectedNamespaces() []string { return []string{ render.ElasticsearchNamespace, render.IntrusionDetectionNamespace, @@ -91,3 +100,13 @@ func ProtectedNamespaces() []string { istio.IstioNamespace, } } + +// MultiTenant reports the tenancy mode resolved at startup. +func (s startup) MultiTenant() bool { + return s.opts.MultiTenant +} + +// Cloud reports whether this binary was built for Calico Cloud. +func (s startup) Cloud() bool { + return s.opts.Cloud +} diff --git a/pkg/enterprise/startup_test.go b/pkg/enterprise/startup_test.go index 1c2eeb0185..e5cfd3f0d0 100644 --- a/pkg/enterprise/startup_test.go +++ b/pkg/enterprise/startup_test.go @@ -24,10 +24,17 @@ import ( operatorv1 "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/enterprise" + eoptions "github.com/tigera/operator/pkg/enterprise/options" + "github.com/tigera/operator/pkg/extensions" "github.com/tigera/operator/pkg/render" "github.com/tigera/operator/pkg/render/logstorage" ) +// startupFor returns the startup extension the operator gets for a variant. +func startupFor(variant operatorv1.ProductVariant) extensions.StartupExtension { + return enterprise.New(variant, eoptions.Options{}).Startup() +} + var _ = Describe("VerifyAPIsExist", func() { enterpriseAPIs := &metav1.APIResourceList{ GroupVersion: "operator.tigera.io/v1", @@ -46,14 +53,14 @@ var _ = Describe("VerifyAPIsExist", func() { cs := fake.NewSimpleClientset() cs.Resources = []*metav1.APIResourceList{enterpriseAPIs} - Expect(enterprise.VerifyAPIsExist(operatorv1.CalicoEnterprise, cs)).To(Succeed()) + Expect(startupFor(operatorv1.CalicoEnterprise).VerifyAPIsExist(cs)).To(Succeed()) }) It("rejects an Enterprise install whose CRDs are absent", func() { cs := fake.NewSimpleClientset() cs.Resources = []*metav1.APIResourceList{calicoAPIs} - Expect(enterprise.VerifyAPIsExist(operatorv1.CalicoEnterprise, cs)).To(MatchError(ContainSubstring("CRDs are not installed"))) + Expect(startupFor(operatorv1.CalicoEnterprise).VerifyAPIsExist(cs)).To(MatchError(ContainSubstring("CRDs are not installed"))) }) It("accepts the deprecated Enterprise variant", func() { @@ -61,18 +68,18 @@ var _ = Describe("VerifyAPIsExist", func() { cs.Resources = []*metav1.APIResourceList{enterpriseAPIs} //nolint:staticcheck // SA1019: the deprecated spelling is what this covers - Expect(enterprise.VerifyAPIsExist(operatorv1.TigeraSecureEnterprise, cs)).To(Succeed()) + Expect(startupFor(operatorv1.TigeraSecureEnterprise).VerifyAPIsExist(cs)).To(Succeed()) }) It("leaves Calico alone when the Enterprise CRDs are absent", func() { cs := fake.NewSimpleClientset() cs.Resources = []*metav1.APIResourceList{calicoAPIs} - Expect(enterprise.VerifyAPIsExist(operatorv1.Calico, cs)).To(Succeed()) + Expect(startupFor(operatorv1.Calico).VerifyAPIsExist(cs)).To(Succeed()) }) }) -var _ = Describe("VerifyElasticsearch", func() { +var _ = Describe("VerifyClusterState", func() { internalCert := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: render.TigeraElasticsearchInternalCertSecret, @@ -89,52 +96,71 @@ var _ = Describe("VerifyElasticsearch", func() { It("rejects an external configuration that still has the internal certificate", func() { cs := fake.NewSimpleClientset(internalCert) - err := enterprise.VerifyElasticsearch(ctx, cs, operatorv1.CalicoEnterprise, false, true) + err := startupFor(operatorv1.CalicoEnterprise).VerifyClusterState(ctx, cs, false, true) Expect(err).To(MatchError(ContainSubstring("configured as external ES"))) }) It("rejects an internal configuration that still has the external certificate", func() { cs := fake.NewSimpleClientset(externalCert) - err := enterprise.VerifyElasticsearch(ctx, cs, operatorv1.CalicoEnterprise, false, false) + err := startupFor(operatorv1.CalicoEnterprise).VerifyClusterState(ctx, cs, false, false) Expect(err).To(MatchError(ContainSubstring("configured as internal ES"))) }) It("accepts an external configuration with only the external certificate", func() { cs := fake.NewSimpleClientset(externalCert) - Expect(enterprise.VerifyElasticsearch(ctx, cs, operatorv1.CalicoEnterprise, false, true)).To(Succeed()) + Expect(startupFor(operatorv1.CalicoEnterprise).VerifyClusterState(ctx, cs, false, true)).To(Succeed()) }) It("accepts an internal configuration with only the internal certificate", func() { cs := fake.NewSimpleClientset(internalCert) - Expect(enterprise.VerifyElasticsearch(ctx, cs, operatorv1.CalicoEnterprise, false, false)).To(Succeed()) + Expect(startupFor(operatorv1.CalicoEnterprise).VerifyClusterState(ctx, cs, false, false)).To(Succeed()) }) It("accepts both certificates while a migration is in flight", func() { cs := fake.NewSimpleClientset(internalCert, externalCert) - Expect(enterprise.VerifyElasticsearch(ctx, cs, operatorv1.CalicoEnterprise, true, true)).To(Succeed()) + Expect(startupFor(operatorv1.CalicoEnterprise).VerifyClusterState(ctx, cs, true, true)).To(Succeed()) }) It("leaves Calico alone when a contradictory certificate exists", func() { cs := fake.NewSimpleClientset(externalCert) - Expect(enterprise.VerifyElasticsearch(ctx, cs, operatorv1.Calico, false, false)).To(Succeed()) + Expect(startupFor(operatorv1.Calico).VerifyClusterState(ctx, cs, false, false)).To(Succeed()) }) }) var _ = Describe("ProtectedNamespaces", func() { It("covers the Enterprise namespaces the operator manages", func() { - Expect(enterprise.ProtectedNamespaces()).To(ContainElements( + Expect(startupFor(operatorv1.CalicoEnterprise).ProtectedNamespaces()).To(ContainElements( render.ElasticsearchNamespace, render.ManagerNamespace, render.LogCollectorNamespace, )) }) + It("protects them on a Calico install too", func() { + Expect(startupFor(operatorv1.Calico).ProtectedNamespaces()).To(ContainElement(render.ElasticsearchNamespace)) + }) + + It("reports back the options it was built with", func() { + s := enterprise.New(operatorv1.CalicoEnterprise, eoptions.Options{MultiTenant: true, Cloud: true}).Startup() + + Expect(s.MultiTenant()).To(BeTrue()) + Expect(s.Cloud()).To(BeTrue()) + }) + + It("leaves tenancy alone for a Calico install", func() { + // A nil clientset is safe precisely because Calico never probes for tenancy. + e, err := enterprise.Build(ctx, operatorv1.Calico, nil, false, false) + + Expect(err).NotTo(HaveOccurred()) + Expect(e.Startup().MultiTenant()).To(BeFalse()) + }) + It("claims no namespace the operator does not manage", func() { - Expect(enterprise.ProtectedNamespaces()).NotTo(ContainElements("kube-system", "default")) + Expect(startupFor(operatorv1.CalicoEnterprise).ProtectedNamespaces()).NotTo(ContainElements("kube-system", "default")) }) }) diff --git a/pkg/extensions/extensions.go b/pkg/extensions/extensions.go index f34e5318c0..32be064a75 100644 --- a/pkg/extensions/extensions.go +++ b/pkg/extensions/extensions.go @@ -27,6 +27,9 @@ type Set struct { Goldmane GoldmaneExtension Whisker WhiskerExtension GatewayAPI GatewayAPIExtension + + // Startup is the variant's hook into operator startup rather than into a controller. + Startup StartupExtension } // Extensions is the variant behavior the operator runs with. The zero value extends @@ -110,3 +113,10 @@ func (e Extensions) GatewayAPI() GatewayAPIExtension { } return e.set.GatewayAPI } + +func (e Extensions) Startup() StartupExtension { + if e.set.Startup == nil { + return noopStartup{} + } + return e.set.Startup +} diff --git a/pkg/extensions/extensions_test.go b/pkg/extensions/extensions_test.go index 81b273d9b6..8c7f8bb63d 100644 --- a/pkg/extensions/extensions_test.go +++ b/pkg/extensions/extensions_test.go @@ -96,6 +96,21 @@ var _ = Describe("the zero value Extensions", func() { create, _ := e.APIServer().Modify(baseComponent(), render.Inputs{}).Objects() Expect(create).To(HaveLen(1)) }) + + It("passes the operator's startup checks", func() { + var e extensions.Extensions + + Expect(e.Startup().VerifyAPIsExist(nil)).To(Succeed()) + Expect(e.Startup().VerifyClusterState(context.Background(), nil, false, false)).To(Succeed()) + Expect(e.Startup().ProtectedNamespaces()).To(BeEmpty()) + }) + + It("reports a single-tenant, non-cloud install", func() { + var e extensions.Extensions + + Expect(e.Startup().MultiTenant()).To(BeFalse()) + Expect(e.Startup().Cloud()).To(BeFalse()) + }) }) var _ = Describe("the base ManagementClusterConnection validation", func() { diff --git a/pkg/extensions/startup.go b/pkg/extensions/startup.go new file mode 100644 index 0000000000..91736aaa9c --- /dev/null +++ b/pkg/extensions/startup.go @@ -0,0 +1,67 @@ +// 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 extensions + +import ( + "context" + + "k8s.io/client-go/kubernetes" +) + +// StartupExtension is the variant's hook into the operator's startup, before any +// controller registers. +type StartupExtension interface { + // VerifyAPIsExist reports whether the APIs the variant's controllers watch are + // served. The operator exits when they are not, so the kubelet retries it once + // the CRDs are installed. + VerifyAPIsExist(cs kubernetes.Interface) error + + // VerifyClusterState rejects a cluster whose existing state contradicts the + // configuration the operator was given. + VerifyClusterState(ctx context.Context, cs kubernetes.Interface, migrating, external bool) error + + // ProtectedNamespaces are the namespaces the variant manages. The operator must + // not run in one of them. + ProtectedNamespaces() []string + + // MultiTenant reports whether the cluster runs the variant in multi-tenant mode. + MultiTenant() bool + + // Cloud reports whether this binary was built for the variant's hosted product. + Cloud() bool +} + +// noopStartup runs the core operator's behavior unchanged. +type noopStartup struct{} + +func (noopStartup) VerifyAPIsExist(kubernetes.Interface) error { + return nil +} + +func (noopStartup) VerifyClusterState(context.Context, kubernetes.Interface, bool, bool) error { + return nil +} + +func (noopStartup) ProtectedNamespaces() []string { + return nil +} + +func (noopStartup) MultiTenant() bool { + return false +} + +func (noopStartup) Cloud() bool { + return false +} diff --git a/version/version.go b/version/version.go index e30ca351a0..38d73fe7bb 100644 --- a/version/version.go +++ b/version/version.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 Tigera, Inc. All rights reserved. +// Copyright (c) 2020-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. @@ -16,3 +16,7 @@ package version // VERSION is filled out during the build process (using git describe output) var VERSION = "unknown" + +// BuildVariant names the product variant baked into this binary, empty for a stock +// build. It is filled out during the build process; see CLOUD_LDFLAGS in the Makefile. +var BuildVariant string