From c41dc6bf57baac6b456afaf7e291127b4abe1f38 Mon Sep 17 00:00:00 2001 From: Casey Davenport Date: Wed, 12 Aug 2026 10:32:00 -0700 Subject: [PATCH 1/2] Carry degrade reasons on the remaining extension errors Restores the per-error status reasons the enterprise controllers used before the extension split. --- pkg/enterprise/apiserver/extension.go | 34 ++++---- pkg/enterprise/apiserver/extension_test.go | 57 ++++++++++++- pkg/enterprise/clusterconnection/extension.go | 7 +- .../clusterconnection/extension_test.go | 30 +++++++ pkg/enterprise/installation/core.go | 21 +++-- pkg/enterprise/installation/core_test.go | 80 ++++++++++++++++++- .../installation/kubecontrollers.go | 13 +-- pkg/enterprise/installation/node.go | 2 +- pkg/enterprise/windows/extension.go | 2 +- pkg/enterprise/windows/extension_test.go | 50 ++++++++++++ 10 files changed, 252 insertions(+), 44 deletions(-) diff --git a/pkg/enterprise/apiserver/extension.go b/pkg/enterprise/apiserver/extension.go index c1c6c5ce4e..a819679f30 100644 --- a/pkg/enterprise/apiserver/extension.go +++ b/pkg/enterprise/apiserver/extension.go @@ -182,22 +182,22 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con trustedBundle, err := ci.CertificateManager.CreateNamedTrustedBundleFromSecrets(render.APIServerResourceName, ci.Client, common.OperatorNamespace(), false) if err != nil { - return ci, nil, fmt.Errorf("unable to create the trusted bundle: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceCreateError, "unable to create the trusted bundle: %w", err) } applicationLayer, err := utils.GetApplicationLayer(ctx, ci.Client) if err != nil { - return ci, nil, fmt.Errorf("error reading ApplicationLayer: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading ApplicationLayer: %w", err) } managementCluster, err := utils.GetManagementCluster(ctx, ci.Client) if err != nil { - return ci, nil, fmt.Errorf("error reading ManagementCluster: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading ManagementCluster: %w", err) } managementClusterConnection, err := utils.GetManagementClusterConnection(ctx, ci.Client) if err != nil { - return ci, nil, fmt.Errorf("error reading ManagementClusterConnection: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading ManagementClusterConnection: %w", err) } if managementCluster != nil && managementClusterConnection != nil { @@ -206,7 +206,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con rbacManagementEnabled, err := utils.RBACManagementEnabled(ctx, ci.Client, e.variant, e.opts.MultiTenant) if err != nil { - return ci, nil, fmt.Errorf("error reading the RBAC management UI ConfigMap: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading the RBAC management UI ConfigMap: %w", err) } // Management cluster only: the apiserver mounts the tunnel CA secret so it can sign @@ -214,13 +214,13 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con // ManagementCluster.Spec.TLS is defaulted; degrade until it exists. if managementCluster != nil && managementCluster.Spec.TLS != nil && !e.opts.MultiTenant { if _, err := utils.GetSecret(ctx, ci.Client, managementCluster.Spec.TLS.SecretName, common.OperatorNamespace()); err != nil { - return ci, nil, fmt.Errorf("unable to fetch the tunnel secret: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "unable to fetch the tunnel secret: %w", err) } } prometheusCertificate, err := ci.CertificateManager.GetCertificate(ci.Client, monitor.PrometheusClientTLSSecretName, common.OperatorNamespace()) if err != nil { - return ci, nil, fmt.Errorf("failed to get certificate: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "failed to get certificate: %w", err) } if prometheusCertificate != nil { trustedBundle.AddCertificates(prometheusCertificate) @@ -229,7 +229,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con if managementClusterConnection != nil { voltronLinseedCert, err := ci.CertificateManager.GetCertificate(ci.Client, render.VoltronLinseedPublicCert, common.OperatorNamespace()) if err != nil { - return ci, nil, fmt.Errorf("failed to retrieve %s: %w", render.VoltronLinseedPublicCert, err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "failed to retrieve %s: %w", render.VoltronLinseedPublicCert, err) } if voltronLinseedCert != nil { trustedBundle.AddCertificates(voltronLinseedCert) @@ -241,13 +241,13 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con var keyValidatorConfig authentication.KeyValidatorConfig authenticationCR, err := utils.GetAuthentication(ctx, ci.Client) if err != nil && !apierrors.IsNotFound(err) { - return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error while fetching Authentication: %s", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error while fetching Authentication: %w", err) } if authenticationCR != nil && authenticationCR.Status.State == operatorv1.TigeraStatusReady { if utils.DexEnabled(authenticationCR) { certificate, err := ci.CertificateManager.GetCertificate(ci.Client, render.DexTLSSecretName, common.OperatorNamespace()) if err != nil { - return ci, nil, extensions.Degradedf(operatorv1.CertificateError, "failed to retrieve %s: %s", render.DexTLSSecretName, err) + return ci, nil, extensions.Degradedf(operatorv1.CertificateError, "failed to retrieve %s: %w", render.DexTLSSecretName, err) } else if certificate == nil { return ci, nil, extensions.NotReadyf("waiting for secret '%s' to become available", render.DexTLSSecretName) } @@ -255,7 +255,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con } keyValidatorConfig, err = utils.GetKeyValidatorConfig(ctx, ci.Client, authenticationCR, ci.RenderInputs.ClusterDomain, false) if err != nil { - return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "failed to get KeyValidator config: %s", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "failed to get KeyValidator config: %w", err) } } @@ -270,7 +270,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con dns.GetServiceDNSNames(render.APIServerServiceName, render.APIServerNamespace, ci.RenderInputs.ClusterDomain), ) if err != nil { - return ci, nil, fmt.Errorf("unable to get or create query server tls key pair: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceCreateError, "unable to get or create query server tls key pair: %w", err) } } @@ -279,11 +279,11 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con // sidecar images are only needed when sidecar injection is enabled. imageSet, err := imageset.GetImageSet(ctx, ci.Client, in.Variant) if err != nil { - return ci, nil, err + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error getting ImageSet: %w", err) } calicoImage, err := components.GetReference(components.CombinedCalicoImage(in), in.Registry, in.ImagePath, in.ImagePrefix, imageSet) if err != nil { - return ci, nil, err + return ci, nil, extensions.Degradedf(operatorv1.ResourceUpdateError, "error with images from ImageSet: %w", err) } var l7EnvoyImage, dikastesImage string @@ -292,11 +292,11 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con *applicationLayer.Spec.SidecarInjection == operatorv1.SidecarEnabled { l7EnvoyImage, err = components.GetReference(components.ComponentEnvoyProxy, in.Registry, in.ImagePath, in.ImagePrefix, imageSet) if err != nil { - return ci, nil, err + return ci, nil, extensions.Degradedf(operatorv1.ResourceUpdateError, "error with images from ImageSet: %w", err) } dikastesImage, err = components.GetReference(components.ComponentDikastes, in.Registry, in.ImagePath, in.ImagePrefix, imageSet) if err != nil { - return ci, nil, err + return ci, nil, extensions.Degradedf(operatorv1.ResourceUpdateError, "error with images from ImageSet: %w", err) } } @@ -308,7 +308,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con if e.opts.MultiTenant { bindingNamespaces, err = utils.TenantNamespaces(ctx, ci.Client, nil) if err != nil { - return ci, nil, fmt.Errorf("error reading tenant namespaces: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading tenant namespaces: %w", err) } } diff --git a/pkg/enterprise/apiserver/extension_test.go b/pkg/enterprise/apiserver/extension_test.go index 7db82e2c78..7d26f55406 100644 --- a/pkg/enterprise/apiserver/extension_test.go +++ b/pkg/enterprise/apiserver/extension_test.go @@ -17,6 +17,7 @@ package apiserver_test import ( "context" "fmt" + "reflect" "slices" . "github.com/onsi/ginkgo/v2" @@ -47,6 +48,7 @@ import ( "github.com/tigera/operator/pkg/extensions/extensionstest" "github.com/tigera/operator/pkg/render" "github.com/tigera/operator/pkg/render/common/rbacmanagement" + "github.com/tigera/operator/pkg/render/monitor" "github.com/tigera/operator/pkg/tls/certificatemanagement" ) @@ -93,15 +95,35 @@ func apiServerScheme() *runtime.Scheme { // failingConfigMapInputs returns controller inputs whose client fails every read of // the named ConfigMap with readErr. func failingConfigMapInputs(name string, readErr error) controller.Inputs { + return failingGetInputs(failingGet{obj: &corev1.ConfigMap{}, name: name}, readErr) +} + +// failingGet names the object read that should fail: any object of the same type as +// obj, narrowed to a single name when one is given. +type failingGet struct { + obj client.Object + name string +} + +func (f failingGet) matches(key client.ObjectKey, obj client.Object) bool { + if reflect.TypeOf(obj) != reflect.TypeOf(f.obj) { + return false + } + return f.name == "" || f.name == key.Name +} + +// failingGetInputs returns controller inputs whose client fails the read fail +// describes with readErr. +func failingGetInputs(fail failingGet, readErr error, objs ...client.Object) controller.Inputs { c := ctrlrfake.DefaultFakeClientBuilder(apiServerScheme()).WithInterceptorFuncs(interceptor.Funcs{ Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { - if _, ok := obj.(*corev1.ConfigMap); ok && key.Name == name { + if fail.matches(key, obj) { return readErr } return c.Get(ctx, key, obj, opts...) }, }).Build() - return apiServerControllerInputsWith(c, operatorv1.CalicoEnterprise, nil) + return apiServerControllerInputsWith(c, operatorv1.CalicoEnterprise, nil, objs...) } // rbacManagementGate builds the admin-owned ConfigMap that switches the RBAC @@ -173,6 +195,34 @@ var _ = Describe("API server enterprise controller extension", func() { }) }) + DescribeTable("reports the degraded reason for the read it failed on", + func(fail failingGet, expected operatorv1.TigeraStatusReason, objs ...client.Object) { + readErr := fmt.Errorf("the API server is having a bad day") + + _, _, err := ext.APIServer().ExtendInputs(ctx, failingGetInputs(fail, readErr, objs...)) + Expect(err).To(MatchError(readErr)) + reason, ok := extensions.DegradedReason(err) + Expect(ok).To(BeTrue()) + Expect(reason).To(Equal(expected)) + }, + Entry("the ApplicationLayer", + failingGet{obj: &operatorv1.ApplicationLayer{}}, operatorv1.ResourceReadError), + Entry("the ManagementCluster", + failingGet{obj: &operatorv1.ManagementCluster{}}, operatorv1.ResourceReadError), + Entry("the ManagementClusterConnection", + failingGet{obj: &operatorv1.ManagementClusterConnection{}}, operatorv1.ResourceReadError), + Entry("the tunnel secret on a management cluster", + failingGet{obj: &corev1.Secret{}, name: render.VoltronTunnelSecretName}, operatorv1.ResourceReadError, + managementCluster()), + Entry("the prometheus client certificate", + failingGet{obj: &corev1.Secret{}, name: monitor.PrometheusClientTLSSecretName}, operatorv1.ResourceReadError), + Entry("the Voltron linseed certificate on a managed cluster", + failingGet{obj: &corev1.Secret{}, name: render.VoltronLinseedPublicCert}, operatorv1.ResourceReadError, + managementClusterConnection()), + Entry("the Authentication", + failingGet{obj: &operatorv1.Authentication{}}, operatorv1.ResourceReadError), + ) + Describe("Dex", func() { readyAuthentication := func() *operatorv1.Authentication { return &operatorv1.Authentication{ @@ -380,6 +430,9 @@ var _ = Describe("API server enterprise modifier", func() { _, _, err := ext.APIServer().ExtendInputs(ctx, ci) Expect(err).To(MatchError(readErr)) + reason, ok := extensions.DegradedReason(err) + Expect(ok).To(BeTrue()) + Expect(reason).To(Equal(operatorv1.ResourceReadError)) }) Context("Calico Cloud", func() { diff --git a/pkg/enterprise/clusterconnection/extension.go b/pkg/enterprise/clusterconnection/extension.go index fc4204bdfa..aa96f6160c 100644 --- a/pkg/enterprise/clusterconnection/extension.go +++ b/pkg/enterprise/clusterconnection/extension.go @@ -16,7 +16,6 @@ package clusterconnection import ( "context" - "fmt" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/client-go/kubernetes" @@ -103,7 +102,7 @@ func (e *Extension) ValidateAndDefault(cr *operatorv1.ManagementClusterConnectio func (e *Extension) validate(ctx context.Context, ci controller.Inputs) error { managementCluster, err := utils.GetManagementCluster(ctx, ci.Client) if err != nil { - return fmt.Errorf("error reading ManagementCluster: %w", err) + return extensions.Degradedf(operatorv1.ResourceReadError, "error reading ManagementCluster: %w", err) } if managementCluster != nil { return extensions.InvalidConfigf("having both a ManagementCluster and a ManagementClusterConnection is not supported") @@ -123,7 +122,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con clusterInformation, err := utils.FetchClusterInformation(ctx, ci.Client) if err != nil { - return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error querying ClusterInformation: %s", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error querying ClusterInformation: %w", err) } // Ensure the license can support enterprise policy before enabling the @@ -132,7 +131,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con if license, err := utils.FetchLicenseKey(ctx, ci.Client); err == nil { includeEgressNetworkPolicy = utils.IsFeatureActive(license, common.EgressAccessControlFeature) } else if !k8serrors.IsNotFound(err) { - return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error querying license: %s", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error querying license: %w", err) } ci.RenderInputs.Extension = render.GuardianRenderData{ diff --git a/pkg/enterprise/clusterconnection/extension_test.go b/pkg/enterprise/clusterconnection/extension_test.go index f6e6782f6f..0004f682ef 100644 --- a/pkg/enterprise/clusterconnection/extension_test.go +++ b/pkg/enterprise/clusterconnection/extension_test.go @@ -15,12 +15,17 @@ package clusterconnection_test import ( + "context" + "fmt" + "reflect" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" v3 "github.com/tigera/api/pkg/apis/projectcalico/v3" @@ -60,6 +65,20 @@ var _ = Describe("clusterconnection enterprise controller extension", func() { return ctrlrfake.DefaultFakeClientBuilder(scheme).WithObjects(objs...).Build() } + // failingGetClient fails every read of an object of the same type as fail. + failingGetClient := func(fail client.Object, readErr error) client.Client { + scheme := runtime.NewScheme() + Expect(apis.AddToScheme(scheme, false)).NotTo(HaveOccurred()) + return ctrlrfake.DefaultFakeClientBuilder(scheme).WithInterceptorFuncs(interceptor.Funcs{ + Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + if reflect.TypeOf(obj) == reflect.TypeOf(fail) { + return readErr + } + return c.Get(ctx, key, obj, opts...) + }, + }).Build() + } + Describe("configuration", func() { It("rejects a cluster that is both a management and a managed cluster", func() { cli = newClient(&operatorv1.ManagementCluster{ObjectMeta: metav1.ObjectMeta{Name: "tigera-secure"}}) @@ -70,6 +89,17 @@ var _ = Describe("clusterconnection enterprise controller extension", func() { Expect(err.Error()).To(ContainSubstring("not supported")) }) + It("degrades with a read error when the ManagementCluster cannot be read", func() { + readErr := fmt.Errorf("the API server is having a bad day") + cli = failingGetClient(&operatorv1.ManagementCluster{}, readErr) + + _, _, err := ext.ClusterConnection().ExtendInputs(ctx, controllerInputs()) + Expect(err).To(MatchError(readErr)) + reason, ok := extensions.DegradedReason(err) + Expect(ok).To(BeTrue()) + Expect(reason).To(Equal(operatorv1.ResourceReadError)) + }) + It("accepts impersonation and defaults it to empty lists", func() { cr := &operatorv1.ManagementClusterConnection{} Expect(ext.ClusterConnection().ValidateAndDefault(cr)).NotTo(HaveOccurred()) diff --git a/pkg/enterprise/installation/core.go b/pkg/enterprise/installation/core.go index 5514692802..6ad6ac1fea 100644 --- a/pkg/enterprise/installation/core.go +++ b/pkg/enterprise/installation/core.go @@ -16,7 +16,6 @@ package installation import ( "context" - "fmt" "strings" v3 "github.com/tigera/api/pkg/apis/projectcalico/v3" @@ -178,7 +177,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con dns.GetServiceDNSNames(render.CalicoNodeMetricsService, common.CalicoNamespace, ci.RenderInputs.ClusterDomain), ) if err != nil { - return ci, nil, fmt.Errorf("error creating node prometheus TLS certificate: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceCreateError, "error creating node prometheus TLS certificate: %w", err) } if nodePrometheusTLS != nil { ci.RenderInputs.TrustedBundle.AddCertificates(nodePrometheusTLS) @@ -194,7 +193,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con dns.GetServiceDNSNames(kubecontrollers.KubeControllerMetrics, common.CalicoNamespace, ci.RenderInputs.ClusterDomain), ) if err != nil { - return ci, nil, fmt.Errorf("error creating kube-controllers metrics TLS certificate: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error finding or creating the kube-controllers metrics TLS certificate: %w", err) } if kubeControllerTLS != nil { ci.RenderInputs.TrustedBundle.AddCertificates(kubeControllerTLS) @@ -202,7 +201,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con logCollector, err := utils.GetLogCollector(ctx, ci.Client) if err != nil { - return ci, nil, fmt.Errorf("error reading LogCollector: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading LogCollector: %w", err) } // calico-kube-controllers enterprise additions: the WAF surface, the enterprise @@ -210,26 +209,26 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con // kube-controllers needs an extra license-push rule. managementClusterConnection, err := utils.GetManagementClusterConnection(ctx, ci.Client) if err != nil { - return ci, nil, fmt.Errorf("error reading ManagementClusterConnection: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading ManagementClusterConnection: %w", err) } managementCluster, err := utils.GetManagementCluster(ctx, ci.Client) if err != nil { - return ci, nil, fmt.Errorf("error reading ManagementCluster: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading ManagementCluster: %w", err) } if managementCluster != nil && managementClusterConnection != nil { return ci, nil, extensions.InvalidConfigf("having both a ManagementCluster and a ManagementClusterConnection is not supported") } waf, wafWebhookTLS, err := buildWAFData(ctx, ci) if err != nil { - return ci, nil, fmt.Errorf("error preparing WAF configuration: %w", err) + return ci, nil, err } // The rbacsync controller reconciles the ClusterRoles backing the Manager UI's // RBAC management feature. rbacManagementEnabled, err := utils.RBACManagementEnabled(ctx, ci.Client, e.variant, e.opts.MultiTenant) if err != nil { - return ci, nil, fmt.Errorf("error reading the RBAC management UI ConfigMap: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading the RBAC management UI ConfigMap: %w", err) } ci.RenderInputs.Extension = installationRenderData{ @@ -246,7 +245,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con prometheusClientCert, err := ci.CertificateManager.GetCertificate(ci.Client, monitor.PrometheusClientTLSSecretName, common.OperatorNamespace()) if err != nil { - return ci, nil, fmt.Errorf("unable to fetch prometheus certificate: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.CertificateError, "unable to fetch prometheus certificate: %w", err) } if prometheusClientCert != nil { ci.RenderInputs.TrustedBundle.AddCertificates(prometheusClientCert) @@ -254,7 +253,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con esgwCertificate, err := ci.CertificateManager.GetCertificate(ci.Client, relasticsearch.PublicCertSecret, common.OperatorNamespace()) if err != nil { - return ci, nil, fmt.Errorf("failed to retrieve / validate %s: %w", relasticsearch.PublicCertSecret, err) + return ci, nil, extensions.Degradedf(operatorv1.CertificateError, "failed to retrieve / validate %s: %w", relasticsearch.PublicCertSecret, err) } if esgwCertificate != nil { ci.RenderInputs.TrustedBundle.AddCertificates(esgwCertificate) @@ -264,7 +263,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con // manager internal cert. managerInternalTLS, err := ci.CertificateManager.GetCertificate(ci.Client, render.ManagerInternalTLSSecretName, common.OperatorNamespace()) if err != nil { - return ci, nil, fmt.Errorf("failed to retrieve %s: %w", render.ManagerInternalTLSSecretName, err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceReadError, "failed to retrieve %s: %w", render.ManagerInternalTLSSecretName, err) } if managerInternalTLS != nil { ci.RenderInputs.TrustedBundle.AddCertificates(managerInternalTLS) diff --git a/pkg/enterprise/installation/core_test.go b/pkg/enterprise/installation/core_test.go index ac1a591c43..527ac68439 100644 --- a/pkg/enterprise/installation/core_test.go +++ b/pkg/enterprise/installation/core_test.go @@ -16,13 +16,17 @@ package installation_test import ( "context" + "fmt" + "reflect" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/controller-runtime/pkg/client" v3 "github.com/tigera/api/pkg/apis/projectcalico/v3" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" operatorv1 "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/apis" @@ -32,7 +36,10 @@ import ( ctrlrfake "github.com/tigera/operator/pkg/ctrlruntime/client/fake" "github.com/tigera/operator/pkg/extensions" "github.com/tigera/operator/pkg/render" + relasticsearch "github.com/tigera/operator/pkg/render/common/elasticsearch" + "github.com/tigera/operator/pkg/render/common/rbacmanagement" "github.com/tigera/operator/pkg/render/kubecontrollers" + "github.com/tigera/operator/pkg/render/monitor" ) var _ = Describe("installation controller extension", func() { @@ -45,7 +52,7 @@ var _ = Describe("installation controller extension", func() { _, _, err := ext.Installation().ExtendInputs(ctx, ci) reason, ok := extensions.DegradedReason(err) Expect(ok).To(BeTrue()) - Expect(reason).To(Equal(operatorv1.ResourceValidationError)) + Expect(reason).To(Equal(operatorv1.InvalidConfigurationError)) }) DescribeTable("defaults dnsTrustedServers for providers whose DNS service isn't kube-dns", @@ -90,13 +97,82 @@ var _ = Describe("installation controller extension", func() { Expect(err).NotTo(HaveOccurred()) Expect(managed).To(BeEmpty()) }) + + DescribeTable("reports the degraded reason for the read it failed on", + func(fail failingGet, expected operatorv1.TigeraStatusReason) { + readErr := fmt.Errorf("the API server is having a bad day") + ci := newControllerInputsWith(failingGetClient(fail, readErr), operatorv1.CalicoEnterprise) + + _, _, err := ext.Installation().ExtendInputs(ctx, ci) + Expect(err).To(MatchError(readErr)) + reason, ok := extensions.DegradedReason(err) + Expect(ok).To(BeTrue()) + Expect(reason).To(Equal(expected)) + }, + Entry("the node prometheus keypair", + failingGet{obj: &corev1.Secret{}, name: render.NodePrometheusTLSServerSecret}, operatorv1.ResourceCreateError), + Entry("the kube-controllers metrics keypair", + failingGet{obj: &corev1.Secret{}, name: kubecontrollers.KubeControllerPrometheusTLSSecret}, operatorv1.ResourceReadError), + Entry("the LogCollector", + failingGet{obj: &operatorv1.LogCollector{}}, operatorv1.ResourceReadError), + Entry("the ManagementClusterConnection", + failingGet{obj: &operatorv1.ManagementClusterConnection{}}, operatorv1.ResourceReadError), + Entry("the ManagementCluster", + failingGet{obj: &operatorv1.ManagementCluster{}}, operatorv1.ResourceReadError), + Entry("the GatewayAPI", + failingGet{obj: &operatorv1.GatewayAPI{}}, operatorv1.ResourceReadError), + Entry("the RBAC management UI ConfigMap", + failingGet{obj: &corev1.ConfigMap{}, name: rbacmanagement.ConfigMapName}, operatorv1.ResourceReadError), + Entry("the prometheus client certificate", + failingGet{obj: &corev1.Secret{}, name: monitor.PrometheusClientTLSSecretName}, operatorv1.CertificateError), + Entry("the elasticsearch gateway certificate", + failingGet{obj: &corev1.Secret{}, name: relasticsearch.PublicCertSecret}, operatorv1.CertificateError), + Entry("the manager internal certificate", + failingGet{obj: &corev1.Secret{}, name: render.ManagerInternalTLSSecretName}, operatorv1.ResourceReadError), + ) }) +// failingGet names the object read that should fail: any object of the same type as +// obj, narrowed to a single name when one is given. +type failingGet struct { + obj client.Object + name string +} + +func (f failingGet) matches(key client.ObjectKey, obj client.Object) bool { + if reflect.TypeOf(obj) != reflect.TypeOf(f.obj) { + return false + } + return f.name == "" || f.name == key.Name +} + +func failingGetClient(fail failingGet, readErr error) client.WithWatch { + return ctrlrfake.DefaultFakeClientBuilder(installationScheme()).WithInterceptorFuncs(interceptor.Funcs{ + Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + if fail.matches(key, obj) { + return readErr + } + return c.Get(ctx, key, obj, opts...) + }, + }).Build() +} + func newControllerInputs(variant operatorv1.ProductVariant, objs ...client.Object) controller.Inputs { + return newControllerInputsWith(newFakeClient(), variant, objs...) +} + +func newFakeClient() client.WithWatch { + return ctrlrfake.DefaultFakeClientBuilder(installationScheme()).Build() +} + +func installationScheme() *runtime.Scheme { scheme := runtime.NewScheme() Expect(apis.AddToScheme(scheme, false)).NotTo(HaveOccurred()) - c := ctrlrfake.DefaultFakeClientBuilder(scheme).Build() + return scheme +} +// newControllerInputsWith is newControllerInputs against a caller-supplied client. +func newControllerInputsWith(c client.WithWatch, variant operatorv1.ProductVariant, objs ...client.Object) controller.Inputs { for _, o := range objs { Expect(c.Create(context.Background(), o)).NotTo(HaveOccurred()) } diff --git a/pkg/enterprise/installation/kubecontrollers.go b/pkg/enterprise/installation/kubecontrollers.go index 6a86c58a75..471fb6ead8 100644 --- a/pkg/enterprise/installation/kubecontrollers.go +++ b/pkg/enterprise/installation/kubecontrollers.go @@ -31,6 +31,7 @@ import ( v3 "github.com/tigera/api/pkg/apis/projectcalico/v3" + operatorv1 "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/common" "github.com/tigera/operator/pkg/components" "github.com/tigera/operator/pkg/controller" @@ -566,9 +567,9 @@ func buildWAFData(ctx context.Context, ci controller.Inputs) (wafRenderData, cer gw, msg, err := gatewayapi.GetGatewayAPI(ctx, ci.Client) if err != nil && !apierrors.IsNotFound(err) { if msg != "" { - return wafRenderData{}, nil, fmt.Errorf("%s: %w", msg, err) + return wafRenderData{}, nil, extensions.Degradedf(operatorv1.ResourceReadError, "%s: %w", msg, err) } - return wafRenderData{}, nil, err + return wafRenderData{}, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error reading GatewayAPI: %w", err) } if gw == nil { return wafRenderData{}, nil, nil @@ -586,11 +587,11 @@ func buildWAFData(ctx context.Context, ci controller.Inputs) (wafRenderData, cer // GetReference the base render uses for every image; the hook has the ImageSet here. imageSet, err := imageset.GetImageSet(ctx, ci.Client, in.Variant) if err != nil { - return wafRenderData{}, nil, err + return wafRenderData{}, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error getting ImageSet: %w", err) } wasmImage, err := components.GetReference(components.ComponentGatewayAPIEnvoyProxy, in.Registry, in.ImagePath, in.ImagePrefix, imageSet) if err != nil { - return wafRenderData{}, nil, err + return wafRenderData{}, nil, extensions.Degradedf(operatorv1.ResourceUpdateError, "error with images from ImageSet: %w", err) } webhookTLS, err := ci.CertificateManager.GetOrCreateKeyPair( @@ -600,12 +601,12 @@ func buildWAFData(ctx context.Context, ci controller.Inputs) (wafRenderData, cer dns.GetServiceDNSNames(applicationlayer.WAFWebhookServiceName, common.CalicoNamespace, ci.RenderInputs.ClusterDomain), ) if err != nil { - return wafRenderData{}, nil, err + return wafRenderData{}, nil, extensions.Degradedf(operatorv1.ResourceCreateError, "error creating the WAF admission webhook TLS certificate: %w", err) } pullSecrets, err := utils.GetInstallationPullSecrets(in, ci.Client) if err != nil { - return wafRenderData{}, nil, err + return wafRenderData{}, nil, extensions.Degradedf(operatorv1.ResourceReadError, "error retrieving pull secrets: %w", err) } var pullSecret *corev1.Secret if len(pullSecrets) > 0 { diff --git a/pkg/enterprise/installation/node.go b/pkg/enterprise/installation/node.go index 61bed69ff2..d29a4ed34c 100644 --- a/pkg/enterprise/installation/node.go +++ b/pkg/enterprise/installation/node.go @@ -255,7 +255,7 @@ func nodeMetricsService(ri render.Inputs) *corev1.Service { // The node and windows controller extensions share it. func ValidateReporterPort(fc *v3.FelixConfiguration) error { if fc != nil && fc.Spec.PrometheusReporterPort != nil && *fc.Spec.PrometheusReporterPort == 0 { - return extensions.InvalidConfigf("felixConfiguration prometheusReporterPort=0 not supported") + return extensions.Degradedf(operatorv1.InvalidConfigurationError, "invalid metrics port: felixConfiguration prometheusReporterPort=0 not supported") } return nil } diff --git a/pkg/enterprise/windows/extension.go b/pkg/enterprise/windows/extension.go index 386cc02327..ad85dfb752 100644 --- a/pkg/enterprise/windows/extension.go +++ b/pkg/enterprise/windows/extension.go @@ -113,7 +113,7 @@ func (e *Extension) ExtendInputs(ctx context.Context, ci controller.Inputs) (con dns.GetServiceDNSNames(render.WindowsNodeMetricsService, common.CalicoNamespace, ci.RenderInputs.ClusterDomain), ) if err != nil { - return ci, nil, fmt.Errorf("error getting node prometheus TLS certificate: %w", err) + return ci, nil, extensions.Degradedf(operatorv1.ResourceCreateError, "error getting node prometheus TLS certificate: %w", err) } ci.RenderInputs.Extension = windowsRenderData{prometheusServerTLS: tls} return ci, nil, nil diff --git a/pkg/enterprise/windows/extension_test.go b/pkg/enterprise/windows/extension_test.go index 4055727d6e..2ab2a7fd4a 100644 --- a/pkg/enterprise/windows/extension_test.go +++ b/pkg/enterprise/windows/extension_test.go @@ -16,15 +16,19 @@ package windows_test import ( "context" + "fmt" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + v3 "github.com/tigera/api/pkg/apis/projectcalico/v3" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" client "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" operatorv1 "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/apis" @@ -123,6 +127,52 @@ var _ = Describe("windows enterprise modifier", func() { Expect(container(ds(out), "node").Env).To(ContainElement(corev1.EnvVar{Name: "FELIX_DNSTRUSTEDSERVERS", Value: "k8s-service:openshift-dns/dns-default"})) }) + It("degrades with a create error when the prometheus reporter keypair cannot be read", func() { + readErr := fmt.Errorf("the API server is having a bad day") + scheme := runtime.NewScheme() + Expect(apis.AddToScheme(scheme, false)).NotTo(HaveOccurred()) + cli := ctrlrfake.DefaultFakeClientBuilder(scheme).Build() + cm, err := certificatemanager.Create(cli, nil, "", common.OperatorNamespace(), certificatemanager.AllowCACreation()) + Expect(err).NotTo(HaveOccurred()) + + failing := ctrlrfake.DefaultFakeClientBuilder(scheme).WithInterceptorFuncs(interceptor.Funcs{ + Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + if _, ok := obj.(*corev1.Secret); ok && key.Name == render.NodePrometheusTLSServerSecret { + return readErr + } + return c.Get(ctx, key, obj, opts...) + }, + }).Build() + + ci := controller.Inputs{ + RenderInputs: render.Inputs{ + Installation: ctxFor(operatorv1.ProviderNone).Installation, + TrustedBundle: cm.CreateTrustedBundle(), + ClusterDomain: dns.DefaultClusterDomain, + }, + Client: failing, + CertificateManager: cm, + } + _, _, err = ext.Windows().ExtendInputs(ctx, ci) + Expect(err).To(MatchError(readErr)) + reason, ok := extensions.DegradedReason(err) + Expect(ok).To(BeTrue()) + Expect(reason).To(Equal(operatorv1.ResourceCreateError)) + }) + + It("rejects a zero prometheus reporter port", func() { + ci := controller.Inputs{ + RenderInputs: render.Inputs{ + Installation: ctxFor(operatorv1.ProviderNone).Installation, + FelixConfiguration: &v3.FelixConfiguration{Spec: v3.FelixConfigurationSpec{PrometheusReporterPort: ptr.To(0)}}, + }, + } + _, _, err := ext.Windows().ExtendInputs(ctx, ci) + reason, ok := extensions.DegradedReason(err) + Expect(ok).To(BeTrue()) + Expect(reason).To(Equal(operatorv1.InvalidConfigurationError)) + }) + It("mounts the prometheus reporter keypair when present", func() { scheme := runtime.NewScheme() Expect(apis.AddToScheme(scheme, false)).NotTo(HaveOccurred()) From 18dcd598ef0e86e3cd47ee093688452e60d0e266 Mon Sep 17 00:00:00 2001 From: Casey Davenport Date: Wed, 12 Aug 2026 11:12:52 -0700 Subject: [PATCH 2/2] Update core controller expectation for the metrics port reason --- pkg/controller/installation/core_controller_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/controller/installation/core_controller_test.go b/pkg/controller/installation/core_controller_test.go index b39cf7c801..c60a01a2af 100644 --- a/pkg/controller/installation/core_controller_test.go +++ b/pkg/controller/installation/core_controller_test.go @@ -399,7 +399,7 @@ var _ = Describe("Testing core-controller installation", func() { }) }) - It("degrades with a validation reason when the extension rejects the configuration", func() { + It("degrades with a configuration reason when the extension rejects the configuration", func() { mockStatus.On("SetDegraded", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return() port := 0 @@ -410,7 +410,7 @@ var _ = Describe("Testing core-controller installation", func() { _, err := r.Reconcile(ctx, reconcile.Request{}) Expect(err).To(HaveOccurred()) - mockStatus.AssertCalled(GinkgoT(), "SetDegraded", operator.ResourceValidationError, "felixConfiguration prometheusReporterPort=0 not supported", mock.Anything, mock.Anything) + mockStatus.AssertCalled(GinkgoT(), "SetDegraded", operator.InvalidConfigurationError, "invalid metrics port: felixConfiguration prometheusReporterPort=0 not supported", mock.Anything, mock.Anything) }) Context("image tests", func() {