From 71e46d259c444c50a172bfa8fef312b41ba115ef Mon Sep 17 00:00:00 2001 From: Rene Dekker Date: Wed, 25 Feb 2026 16:13:17 -0800 Subject: [PATCH 1/7] Add labels and annotations to TLS secrets for discoverability Surface certificate metadata (issuer, expiry, DNS SANs, IP SANs) as annotations and add a signer label on TLS secrets produced by Secret() and CreateSelfSignedSecret(). Use certificates.operator.tigera.io prefix for all cert metadata. Exclude certificates.operator.tigera.io keys from MergeMaps to prevent stale operator state from overwriting cluster state. Add unit tests. Co-Authored-By: Claude Opus 4.6 --- pkg/common/components.go | 7 +- .../certificatemanager_test.go | 46 +++++++ .../certificatemanagement_suite_test.go | 33 +++++ pkg/tls/certificatemanagement/keypair.go | 70 ++++++++++- pkg/tls/certificatemanagement/keypair_test.go | 115 ++++++++++++++++++ pkg/tls/certificatemanagement/tls.go | 8 +- 6 files changed, 271 insertions(+), 8 deletions(-) create mode 100644 pkg/tls/certificatemanagement/certificatemanagement_suite_test.go create mode 100644 pkg/tls/certificatemanagement/keypair_test.go diff --git a/pkg/common/components.go b/pkg/common/components.go index 4a2eab59c9..f021d50c6b 100644 --- a/pkg/common/components.go +++ b/pkg/common/components.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022-2024 Tigera, Inc. All rights reserved. +// Copyright (c) 2022-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. @@ -22,11 +22,12 @@ import ( // MergeMaps merges current and desired maps. If both current and desired maps contain the same key, the // desired map's value is used. -// MergeMaps does not copy hash.operator.tigera.io annotations from the current map, since those are managed by the operator. +// MergeMaps does not copy hash.operator.tigera.io or certificates.operator.tigera.io annotations from the +// current map, since those are managed by the operator. func MergeMaps(current, desired map[string]string) map[string]string { for k, v := range current { // Copy over key/value that should be copied. - if _, ok := desired[k]; !ok && !strings.Contains(k, "hash.operator.tigera.io") { + if _, ok := desired[k]; !ok && !strings.Contains(k, "hash.operator.tigera.io") && !strings.Contains(k, "certificates.operator.tigera.io") { desired[k] = v } } diff --git a/pkg/controller/certificatemanager/certificatemanager_test.go b/pkg/controller/certificatemanager/certificatemanager_test.go index b318a2d498..0908d15bf7 100644 --- a/pkg/controller/certificatemanager/certificatemanager_test.go +++ b/pkg/controller/certificatemanager/certificatemanager_test.go @@ -432,6 +432,52 @@ var _ = Describe("Test CertificateManagement suite", func() { Expect(len(keyPair.HashAnnotationValue())).NotTo(BeNil()) }) + It("should add cert metadata labels and annotations to the secret", func() { + By("creating a key pair signed by certificateManager") + keyPair, err := certificateManager.GetOrCreateKeyPair(cli, appSecretName, appNs, appDNSNames) + Expect(err).NotTo(HaveOccurred()) + secret := keyPair.Secret(appNs) + + By("verifying the signer label is set") + Expect(secret.Labels).To(HaveKey("certificates.operator.tigera.io/signer")) + signerLabel := secret.Labels["certificates.operator.tigera.io/signer"] + Expect(signerLabel).To(ContainSubstring("tigera-operator-signer")) + + By("verifying cert metadata annotations are present and correct") + Expect(secret.Annotations).To(HaveKey("certificates.operator.tigera.io/issuer")) + Expect(secret.Annotations).To(HaveKey("certificates.operator.tigera.io/signer")) + Expect(secret.Annotations).To(HaveKey("certificates.operator.tigera.io/expiry")) + Expect(secret.Annotations["certificates.operator.tigera.io/issuer"]).To(ContainSubstring("tigera-operator-signer")) + Expect(secret.Annotations["certificates.operator.tigera.io/signer"]).To(Equal(secret.Annotations["certificates.operator.tigera.io/issuer"])) + Expect(secret.Annotations["certificates.operator.tigera.io/expiry"]).To(MatchRegexp(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$`)) + + By("verifying DNS names annotation contains expected names") + Expect(secret.Annotations).To(HaveKey("certificates.operator.tigera.io/dns-names")) + Expect(secret.Annotations["certificates.operator.tigera.io/dns-names"]).To(ContainSubstring(appSecretName)) + + By("verifying the hash annotation is on the secret") + Expect(secret.Annotations).To(HaveKey(keyPair.HashAnnotationKey())) + Expect(secret.Annotations[keyPair.HashAnnotationKey()]).To(Equal(keyPair.HashAnnotationValue())) + }) + + It("should add cert metadata for BYO secrets", func() { + By("creating a BYO secret and fetching it via certificateManager") + Expect(cli.Create(ctx, byoSecret)).NotTo(HaveOccurred()) + keyPair, err := certificateManager.GetOrCreateKeyPair(cli, appSecretName, appNs, appDNSNames) + Expect(err).NotTo(HaveOccurred()) + Expect(keyPair.BYO()).To(BeTrue()) + secret := keyPair.Secret(appNs) + + By("verifying the signer label reflects the BYO CA") + Expect(secret.Labels).To(HaveKey("certificates.operator.tigera.io/signer")) + Expect(secret.Labels["certificates.operator.tigera.io/signer"]).To(ContainSubstring("byo-ca")) + + By("verifying cert metadata annotations reflect the BYO certificate") + Expect(secret.Annotations).To(HaveKey("certificates.operator.tigera.io/issuer")) + Expect(secret.Annotations["certificates.operator.tigera.io/issuer"]).To(ContainSubstring("byo-ca")) + Expect(secret.Annotations).To(HaveKey("certificates.operator.tigera.io/expiry")) + }) + It("renders the right spec for certificate management", func() { By("creating a key pair w/ certificate management") installation.CertificateManagement = cm diff --git a/pkg/tls/certificatemanagement/certificatemanagement_suite_test.go b/pkg/tls/certificatemanagement/certificatemanagement_suite_test.go new file mode 100644 index 0000000000..c2cb89f215 --- /dev/null +++ b/pkg/tls/certificatemanagement/certificatemanagement_suite_test.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 certificatemanagement_test + +import ( + "testing" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + uzap "go.uber.org/zap" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +func TestCertificateManagement(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/tls_certificatemanagement_suite.xml" + ginkgo.RunSpecs(t, "pkg/tls/certificatemanagement Suite", suiteConfig, reporterConfig) +} diff --git a/pkg/tls/certificatemanagement/keypair.go b/pkg/tls/certificatemanagement/keypair.go index dd328bceef..e40b050637 100644 --- a/pkg/tls/certificatemanagement/keypair.go +++ b/pkg/tls/certificatemanagement/keypair.go @@ -19,6 +19,8 @@ import ( "encoding/pem" "errors" "fmt" + "net" + "strings" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -79,10 +81,21 @@ func (k *KeyPair) Secret(namespace string) *corev1.Secret { } data[corev1.TLSPrivateKeyKey] = k.PrivateKeyPEM data[corev1.TLSCertKey] = k.CertificatePEM + + labels, annotations := tlsSecretMetadata(k.CertificatePEM) + if v := k.HashAnnotationValue(); v != "" { + annotations[k.HashAnnotationKey()] = v + } + return &corev1.Secret{ - TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"}, - ObjectMeta: metav1.ObjectMeta{Name: k.GetName(), Namespace: namespace}, - Data: data, + TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: k.GetName(), + Namespace: namespace, + Labels: labels, + Annotations: annotations, + }, + Data: data, } } @@ -147,6 +160,57 @@ func (k *KeyPair) InitContainer(namespace string, securityContext *corev1.Securi return initContainer } +// tlsSecretMetadata parses certPEM and returns labels and annotations that surface +// certificate metadata on the owning Secret. On parse failure it returns empty maps. +func tlsSecretMetadata(certPEM []byte) (labels map[string]string, annotations map[string]string) { + labels = map[string]string{} + annotations = map[string]string{} + + cert, err := ParseCertificate(certPEM) + if err != nil { + return labels, annotations + } + + // --- labels --- + labels["certificates.operator.tigera.io/signer"] = signerLabelValue(cert.Issuer.CommonName) + + // --- annotations --- + issuerCN := cert.Issuer.CommonName + annotations["certificates.operator.tigera.io/issuer"] = issuerCN + annotations["certificates.operator.tigera.io/signer"] = issuerCN + annotations["certificates.operator.tigera.io/expiry"] = cert.NotAfter.UTC().Format("2006-01-02T15:04:05Z") + + if len(cert.DNSNames) > 0 { + annotations["certificates.operator.tigera.io/dns-names"] = strings.Join(cert.DNSNames, ",") + } + if len(cert.IPAddresses) > 0 { + annotations["certificates.operator.tigera.io/ip-sans"] = strings.Join(ipStrings(cert.IPAddresses), ",") + } + + return labels, annotations +} + +// signerLabelValue returns a Kubernetes-label-safe signer identifier from the +// issuer CN, truncated to 63 chars to satisfy the label value length limit. +func signerLabelValue(cn string) string { + if cn == "" { + return "unknown" + } + if len(cn) > 63 { + return cn[:63] + } + return cn +} + +// ipStrings converts a slice of net.IP to their string representations. +func ipStrings(ips []net.IP) []string { + out := make([]string, len(ips)) + for i, ip := range ips { + out[i] = ip.String() + } + return out +} + func ParseCertificate(certBytes []byte) (*x509.Certificate, error) { pemBlock, _ := pem.Decode(certBytes) if pemBlock == nil { diff --git a/pkg/tls/certificatemanagement/keypair_test.go b/pkg/tls/certificatemanagement/keypair_test.go new file mode 100644 index 0000000000..1995399be9 --- /dev/null +++ b/pkg/tls/certificatemanagement/keypair_test.go @@ -0,0 +1,115 @@ +// 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 certificatemanagement_test + +import ( + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/tigera/operator/pkg/tls/certificatemanagement" +) + +var _ = Describe("TLS secret metadata", func() { + Describe("KeyPair.Secret()", func() { + It("should add labels and annotations to the secret from a valid cert", func() { + secret, err := certificatemanagement.CreateSelfSignedSecret("test-secret", "test-ns", "test-cn", []string{"foo.example.com", "bar.example.com"}) + Expect(err).NotTo(HaveOccurred()) + + kp := certificatemanagement.NewKeyPair(secret, []string{"foo.example.com"}, "cluster.local") + s := kp.Secret("test-ns") + + By("verifying labels are present") + Expect(s.Labels).To(HaveKey("certificates.operator.tigera.io/signer")) + + By("verifying annotations are present") + Expect(s.Annotations).To(HaveKey("certificates.operator.tigera.io/issuer")) + Expect(s.Annotations).To(HaveKey("certificates.operator.tigera.io/signer")) + Expect(s.Annotations).To(HaveKey("certificates.operator.tigera.io/expiry")) + Expect(s.Annotations).To(HaveKey("certificates.operator.tigera.io/dns-names")) + + By("verifying the issuer matches the CN we used") + Expect(s.Annotations["certificates.operator.tigera.io/issuer"]).To(Equal("test-cn")) + Expect(s.Annotations["certificates.operator.tigera.io/signer"]).To(Equal("test-cn")) + + By("verifying the DNS names annotation contains our DNS names") + dnsNames := s.Annotations["certificates.operator.tigera.io/dns-names"] + Expect(dnsNames).To(ContainSubstring("foo.example.com")) + Expect(dnsNames).To(ContainSubstring("bar.example.com")) + + By("verifying cert-expiry is in RFC3339 format") + Expect(s.Annotations["certificates.operator.tigera.io/expiry"]).To(MatchRegexp(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$`)) + + By("verifying the hash annotation is present") + Expect(s.Annotations).To(HaveKey(kp.HashAnnotationKey())) + Expect(s.Annotations[kp.HashAnnotationKey()]).To(Equal(kp.HashAnnotationValue())) + }) + + It("should omit cert-ip-sans when there are no IP SANs", func() { + secret, err := certificatemanagement.CreateSelfSignedSecret("test-secret", "test-ns", "test-cn", []string{"foo.example.com"}) + Expect(err).NotTo(HaveOccurred()) + + kp := certificatemanagement.NewKeyPair(secret, []string{"foo.example.com"}, "cluster.local") + s := kp.Secret("test-ns") + + Expect(s.Annotations).NotTo(HaveKey("certificates.operator.tigera.io/ip-sans")) + }) + + It("should only have hash annotation when cert PEM is empty", func() { + kp := &certificatemanagement.KeyPair{ + Name: "empty-cert", + PrivateKeyPEM: []byte("fake-key"), + CertificatePEM: []byte{}, + } + s := kp.Secret("test-ns") + + Expect(s.Labels).To(BeEmpty()) + Expect(s.Annotations).To(HaveLen(1)) + Expect(s.Annotations).To(HaveKey(kp.HashAnnotationKey())) + }) + }) + + Describe("CreateSelfSignedSecret()", func() { + It("should include labels and annotations on the created secret", func() { + secret, err := certificatemanagement.CreateSelfSignedSecret("my-secret", "my-ns", "my-issuer@1234567890", []string{"dns1.example.com"}) + Expect(err).NotTo(HaveOccurred()) + + By("verifying labels") + Expect(secret.Labels).To(HaveKeyWithValue("certificates.operator.tigera.io/signer", "my-issuer@1234567890")) + + By("verifying annotations") + Expect(secret.Annotations["certificates.operator.tigera.io/issuer"]).To(Equal("my-issuer@1234567890")) + Expect(secret.Annotations["certificates.operator.tigera.io/signer"]).To(Equal("my-issuer@1234567890")) + Expect(secret.Annotations).To(HaveKey("certificates.operator.tigera.io/expiry")) + Expect(secret.Annotations["certificates.operator.tigera.io/dns-names"]).To(Equal("dns1.example.com")) + }) + + It("should handle the signer label with no @ in the CN", func() { + secret, err := certificatemanagement.CreateSelfSignedSecret("my-secret", "my-ns", "simple-signer", nil) + Expect(err).NotTo(HaveOccurred()) + + Expect(secret.Labels["certificates.operator.tigera.io/signer"]).To(Equal("simple-signer")) + }) + + It("should truncate the signer label to 63 characters", func() { + longCN := strings.Repeat("a", 100) + secret, err := certificatemanagement.CreateSelfSignedSecret("my-secret", "my-ns", longCN, nil) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(secret.Labels["certificates.operator.tigera.io/signer"])).To(Equal(63)) + }) + }) +}) diff --git a/pkg/tls/certificatemanagement/tls.go b/pkg/tls/certificatemanagement/tls.go index d3ff6ecd6c..61a0023c3e 100644 --- a/pkg/tls/certificatemanagement/tls.go +++ b/pkg/tls/certificatemanagement/tls.go @@ -60,11 +60,15 @@ func CreateSelfSignedSecret(secretName, namespace, cn string, altNames []string) if err := pem.Encode(&certPem, &pem.Block{Type: blockTypeCert, Bytes: cert}); err != nil { panic(err) } + labels, annotations := tlsSecretMetadata(certPem.Bytes()) + return &corev1.Secret{ TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"}, ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: namespace, + Name: secretName, + Namespace: namespace, + Labels: labels, + Annotations: annotations, }, Data: map[string][]byte{ corev1.TLSCertKey: certPem.Bytes(), From 798a12ceb3e98d2651289c23bcef445297694c8f Mon Sep 17 00:00:00 2001 From: Rene Dekker Date: Fri, 6 Mar 2026 11:42:52 -0800 Subject: [PATCH 2/7] Add Message column to TigeraStatus kubectl output Display the Degraded condition's message when running `kubectl get tigerastatus`, making it easier to see error details at a glance without needing to describe the resource. Co-Authored-By: Claude Opus 4.6 --- api/v1/tigerastatus_types.go | 1 + .../crds/operator/operator.tigera.io_tigerastatuses.yaml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/api/v1/tigerastatus_types.go b/api/v1/tigerastatus_types.go index eddb7c1f51..9206a9bac9 100644 --- a/api/v1/tigerastatus_types.go +++ b/api/v1/tigerastatus_types.go @@ -45,6 +45,7 @@ type TigeraStatusStatus struct { // +kubebuilder:printcolumn:name="Progressing",type="string",JSONPath=".status.conditions[?(@.type=='Progressing')].status",description="Whether the component is processing changes." // +kubebuilder:printcolumn:name="Degraded",type="string",JSONPath=".status.conditions[?(@.type=='Degraded')].status",description="Whether the component is degraded." // +kubebuilder:printcolumn:name="Since",type="date",JSONPath=".status.conditions[?(@.type=='Available')].lastTransitionTime",description="The time the component's Available status last changed." +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type=='Degraded')].message",description="Error message when the component is degraded.",priority=0 type TigeraStatus struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/pkg/imports/crds/operator/operator.tigera.io_tigerastatuses.yaml b/pkg/imports/crds/operator/operator.tigera.io_tigerastatuses.yaml index 282b0a1e6c..838626ba16 100644 --- a/pkg/imports/crds/operator/operator.tigera.io_tigerastatuses.yaml +++ b/pkg/imports/crds/operator/operator.tigera.io_tigerastatuses.yaml @@ -30,6 +30,10 @@ spec: jsonPath: .status.conditions[?(@.type=='Available')].lastTransitionTime name: Since type: date + - description: Error message when the component is degraded. + jsonPath: .status.conditions[?(@.type=='Degraded')].message + name: Message + type: string name: v1 schema: openAPIV3Schema: From 28883777017c035c0687a587649da8d4070cd673 Mon Sep 17 00:00:00 2001 From: Rene Dekker Date: Fri, 6 Mar 2026 11:56:10 -0800 Subject: [PATCH 3/7] Add Warnings() to KeyPairInterface and warning support to StatusManager KeyPairInterface now exposes a Warnings() method that returns a message when a BYO certificate is expiring within 30 days. StatusManager gains SetWarning/ClearWarning methods that append warning text to the Available condition message in TigeraStatus. Co-Authored-By: Claude Opus 4.6 --- pkg/controller/status/mock.go | 10 ++- pkg/controller/status/status.go | 48 +++++++++++++- pkg/controller/status/status_test.go | 42 +++++++++++++ pkg/tls/certificatemanagement/interface.go | 3 + pkg/tls/certificatemanagement/keypair.go | 17 +++++ pkg/tls/certificatemanagement/keypair_test.go | 62 +++++++++++++++++++ 6 files changed, 180 insertions(+), 2 deletions(-) diff --git a/pkg/controller/status/mock.go b/pkg/controller/status/mock.go index 3e70f301ff..048f58267d 100644 --- a/pkg/controller/status/mock.go +++ b/pkg/controller/status/mock.go @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2024 Tigera, Inc. All rights reserved. +// Copyright (c) 2019-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. @@ -99,6 +99,14 @@ func (m *MockStatus) ClearDegraded() { m.Called() } +func (m *MockStatus) SetWarning(key string, msg string) { + m.Called(key, msg) +} + +func (m *MockStatus) ClearWarning(key string) { + m.Called(key) +} + func (m *MockStatus) IsAvailable() bool { return m.Called().Bool(0) } diff --git a/pkg/controller/status/status.go b/pkg/controller/status/status.go index 60a26a57c0..86a8c598ff 100644 --- a/pkg/controller/status/status.go +++ b/pkg/controller/status/status.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "reflect" + "sort" "strings" "sync" "time" @@ -71,6 +72,8 @@ type StatusManager interface { RemoveCertificateSigningRequests(name string) SetDegraded(reason operator.TigeraStatusReason, msg string, err error, log logr.Logger) ClearDegraded() + SetWarning(key string, msg string) + ClearWarning(key string) IsAvailable() bool IsProgressing() bool IsDegraded() bool @@ -95,6 +98,9 @@ type statusManager struct { explicitDegradedMsg string explicitDegradedReason operator.TigeraStatusReason + // warnings stores warning messages keyed by component/secret name. + warnings map[string]string + // Keep track of currently calculated status. progressing []string failing []string @@ -132,6 +138,7 @@ func New(client client.Client, component string, kubernetesVersion *common.Versi statefulsets: make(map[string]types.NamespacedName), cronjobs: make(map[string]types.NamespacedName), certificatestatusrequests: make(map[string]map[string]string), + warnings: make(map[string]string), kubernetesVersion: kubernetesVersion, crExists: crExists, } @@ -161,7 +168,11 @@ func (m *statusManager) updateStatus() { // Now, use that to update the TigeraStatus object for this manager. available := m.IsAvailable() if m.IsAvailable() { - m.setAvailable(operator.AllObjectsAvailable, "All objects available") + msg := "All objects available" + if w := m.warningMessage(); w != "" { + msg = msg + "; " + w + } + m.setAvailable(operator.AllObjectsAvailable, msg) } else { m.clearAvailable() } @@ -262,6 +273,7 @@ func (m *statusManager) OnCRNotFound() { m.deployments = make(map[string]types.NamespacedName) m.statefulsets = make(map[string]types.NamespacedName) m.cronjobs = make(map[string]types.NamespacedName) + m.warnings = make(map[string]string) } // AddDaemonsets tells the status manager to monitor the health of the given daemonsets. @@ -377,6 +389,40 @@ func (m *statusManager) ClearDegraded() { m.explicitDegradedMsg = "" } +// SetWarning sets a warning message for the given key. Warnings are appended to the Available +// condition message so they are visible in `kubectl get tigerastatus`. +func (m *statusManager) SetWarning(key string, msg string) { + m.lock.Lock() + defer m.lock.Unlock() + m.warnings[key] = msg +} + +// ClearWarning removes the warning for the given key. +func (m *statusManager) ClearWarning(key string) { + m.lock.Lock() + defer m.lock.Unlock() + delete(m.warnings, key) +} + +// warningMessage returns all warning messages joined by "; ", or empty if there are none. +func (m *statusManager) warningMessage() string { + m.lock.Lock() + defer m.lock.Unlock() + if len(m.warnings) == 0 { + return "" + } + keys := make([]string, 0, len(m.warnings)) + for k := range m.warnings { + keys = append(keys, k) + } + sort.Strings(keys) + msgs := make([]string, 0, len(keys)) + for _, k := range keys { + msgs = append(msgs, m.warnings[k]) + } + return strings.Join(msgs, "; ") +} + // IsAvailable returns true if the component is available and false otherwise. func (m *statusManager) IsAvailable() bool { m.lock.Lock() diff --git a/pkg/controller/status/status_test.go b/pkg/controller/status/status_test.go index c503e10508..5484e32e0c 100644 --- a/pkg/controller/status/status_test.go +++ b/pkg/controller/status/status_test.go @@ -442,6 +442,48 @@ var _ = Describe("Status reporting tests", func() { Expect(sm.IsProgressing()).To(BeFalse()) }) + It("should include warnings in Available message", func() { + sm.ReadyToMonitor() + Expect(sm.IsAvailable()).To(BeTrue()) + + sm.SetWarning("cert-a", "BYO certificate \"a\" expires in 10 days") + sm.updateStatus() + + stat := &operator.TigeraStatus{} + err := client.Get(context.TODO(), types.NamespacedName{Name: "test-component"}, stat) + Expect(err).NotTo(HaveOccurred()) + for _, c := range stat.Status.Conditions { + if c.Type == operator.ComponentAvailable && c.Status == operator.ConditionTrue { + Expect(c.Message).To(ContainSubstring("All objects available")) + Expect(c.Message).To(ContainSubstring("BYO certificate \"a\" expires in 10 days")) + } + } + }) + + It("should clear warnings from Available message", func() { + sm.ReadyToMonitor() + sm.SetWarning("cert-a", "BYO certificate \"a\" expires in 10 days") + sm.updateStatus() + sm.ClearWarning("cert-a") + sm.updateStatus() + + stat := &operator.TigeraStatus{} + err := client.Get(context.TODO(), types.NamespacedName{Name: "test-component"}, stat) + Expect(err).NotTo(HaveOccurred()) + for _, c := range stat.Status.Conditions { + if c.Type == operator.ComponentAvailable && c.Status == operator.ConditionTrue { + Expect(c.Message).To(Equal("All objects available")) + } + } + }) + + It("should sort multiple warnings deterministically", func() { + sm.ReadyToMonitor() + sm.SetWarning("cert-b", "warning B") + sm.SetWarning("cert-a", "warning A") + Expect(sm.warningMessage()).To(Equal("warning A; warning B")) + }) + It("should prioritize explicit degraded reason over pod failure", func() { Expect(sm.degradedReason()).To(Equal(operator.Unknown)) sm.failing = []string{"This pod has died"} diff --git a/pkg/tls/certificatemanagement/interface.go b/pkg/tls/certificatemanagement/interface.go index 9d8b403d6d..903caf47ca 100644 --- a/pkg/tls/certificatemanagement/interface.go +++ b/pkg/tls/certificatemanagement/interface.go @@ -59,6 +59,9 @@ type KeyPairInterface interface { Secret(namespace string) *corev1.Secret HashAnnotationKey() string HashAnnotationValue() string + // Warnings returns a warning message if the certificate requires attention (e.g., a BYO secret + // expiring within 30 days). Returns an empty string if there are no warnings. + Warnings() string CertificateInterface } diff --git a/pkg/tls/certificatemanagement/keypair.go b/pkg/tls/certificatemanagement/keypair.go index e40b050637..38121722b6 100644 --- a/pkg/tls/certificatemanagement/keypair.go +++ b/pkg/tls/certificatemanagement/keypair.go @@ -21,6 +21,7 @@ import ( "fmt" "net" "strings" + "time" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -113,6 +114,22 @@ func (k *KeyPair) HashAnnotationValue() string { return rmeta.AnnotationHash(rmeta.AnnotationHash(k.CertificatePEM)) } +// Warnings returns a warning string if this is a BYO certificate expiring within 30 days. +func (k *KeyPair) Warnings() string { + if !k.BYO() { + return "" + } + cert, err := ParseCertificate(k.CertificatePEM) + if err != nil { + return "" + } + remaining := time.Until(cert.NotAfter) + if remaining <= 30*24*time.Hour { + return fmt.Sprintf("BYO certificate %q expires in %d days", k.Name, int(remaining.Hours()/24)) + } + return "" +} + func (k *KeyPair) Volume() corev1.Volume { volumeSource := CertificateVolumeSource(k.CertificateManagement, k.GetName()) return corev1.Volume{ diff --git a/pkg/tls/certificatemanagement/keypair_test.go b/pkg/tls/certificatemanagement/keypair_test.go index 1995399be9..262e87c04f 100644 --- a/pkg/tls/certificatemanagement/keypair_test.go +++ b/pkg/tls/certificatemanagement/keypair_test.go @@ -15,7 +15,15 @@ package certificatemanagement_test import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" "strings" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -82,6 +90,60 @@ var _ = Describe("TLS secret metadata", func() { }) }) + Describe("KeyPair.Warnings()", func() { + createCertPEM := func(notAfter time.Time) []byte { + key, err := rsa.GenerateKey(rand.Reader, 2048) + Expect(err).NotTo(HaveOccurred()) + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: notAfter, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + Expect(err).NotTo(HaveOccurred()) + var buf bytes.Buffer + Expect(pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: der})).NotTo(HaveOccurred()) + return buf.Bytes() + } + + It("should return a warning for a BYO cert expiring within 30 days", func() { + kp := &certificatemanagement.KeyPair{ + Name: "my-tls-secret", + CertificatePEM: createCertPEM(time.Now().Add(10 * 24 * time.Hour)), + } + Expect(kp.BYO()).To(BeTrue()) + warning := kp.Warnings() + Expect(warning).To(ContainSubstring("BYO certificate")) + Expect(warning).To(ContainSubstring("my-tls-secret")) + Expect(warning).To(ContainSubstring("expires in")) + }) + + It("should return empty for a BYO cert expiring in more than 30 days", func() { + kp := &certificatemanagement.KeyPair{ + Name: "my-tls-secret", + CertificatePEM: createCertPEM(time.Now().Add(60 * 24 * time.Hour)), + } + Expect(kp.BYO()).To(BeTrue()) + Expect(kp.Warnings()).To(BeEmpty()) + }) + + It("should return empty for a non-BYO cert even if expiring soon", func() { + secret, err := certificatemanagement.CreateSelfSignedSecret("test-secret", "test-ns", "test-cn", nil) + Expect(err).NotTo(HaveOccurred()) + // NewKeyPair creates a BYO keypair (no issuer, no cert management) + // but a KeyPair with an Issuer is not BYO. + issuer := certificatemanagement.NewKeyPair(secret, nil, "cluster.local") + kp := &certificatemanagement.KeyPair{ + Name: "managed-secret", + CertificatePEM: createCertPEM(time.Now().Add(10 * 24 * time.Hour)), + Issuer: issuer, + } + Expect(kp.BYO()).To(BeFalse()) + Expect(kp.Warnings()).To(BeEmpty()) + }) + }) + Describe("CreateSelfSignedSecret()", func() { It("should include labels and annotations on the created secret", func() { secret, err := certificatemanagement.CreateSelfSignedSecret("my-secret", "my-ns", "my-issuer@1234567890", []string{"dns1.example.com"}) From 6450636a48c7803dbf78b2ac00823ff71cdea263 Mon Sep 17 00:00:00 2001 From: Rene Dekker Date: Fri, 6 Mar 2026 11:59:17 -0800 Subject: [PATCH 4/7] Wire up BYO certificate expiry warnings in core_controller Check Warnings() on all keypairs obtained during reconciliation (typha, node, nodePrometheus, kubeController) and propagate them to the status manager so they appear in `kubectl get tigerastatus`. Co-Authored-By: Claude Opus 4.6 --- pkg/controller/installation/core_controller.go | 15 +++++++++++++++ .../installation/core_controller_test.go | 10 ++++++++++ 2 files changed, 25 insertions(+) diff --git a/pkg/controller/installation/core_controller.go b/pkg/controller/installation/core_controller.go index 2b5a550046..16ad6967b4 100644 --- a/pkg/controller/installation/core_controller.go +++ b/pkg/controller/installation/core_controller.go @@ -1696,6 +1696,21 @@ func (r *ReconcileInstallation) Reconcile(ctx context.Context, request reconcile // Tell the status manager that we're ready to monitor the resources we've told it about and receive statuses. r.status.ReadyToMonitor() + // Check BYO certificate expiry warnings and propagate them to the status manager. + for _, kp := range []certificatemanagement.KeyPairInterface{ + typhaNodeTLS.TyphaSecret, typhaNodeTLS.NodeSecret, typhaNodeTLS.TyphaSecretNonClusterHost, + nodePrometheusTLS, kubeControllerTLS, + } { + if kp == nil { + continue + } + if w := kp.Warnings(); w != "" { + r.status.SetWarning(kp.GetName(), w) + } else { + r.status.ClearWarning(kp.GetName()) + } + } + // We can clear the degraded state now since as far as we know everything is in order. r.status.ClearDegraded() diff --git a/pkg/controller/installation/core_controller_test.go b/pkg/controller/installation/core_controller_test.go index 246cce62d2..6964f531d9 100644 --- a/pkg/controller/installation/core_controller_test.go +++ b/pkg/controller/installation/core_controller_test.go @@ -168,6 +168,8 @@ var _ = Describe("Testing core-controller installation", func() { mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("AddCertificateSigningRequests", mock.Anything) mockStatus.On("RemoveCertificateSigningRequests", mock.Anything) mockStatus.On("ReadyToMonitor") @@ -785,6 +787,8 @@ var _ = Describe("Testing core-controller installation", func() { mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("AddCertificateSigningRequests", mock.Anything) mockStatus.On("RemoveCertificateSigningRequests", mock.Anything) mockStatus.On("ReadyToMonitor") @@ -1004,6 +1008,8 @@ var _ = Describe("Testing core-controller installation", func() { mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("AddCertificateSigningRequests", mock.Anything) mockStatus.On("ReadyToMonitor") mockStatus.On("SetMetaData", mock.Anything).Return() @@ -2139,6 +2145,8 @@ var _ = Describe("Testing core-controller installation", func() { mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("AddCertificateSigningRequests", mock.Anything) mockStatus.On("RemoveCertificateSigningRequests", mock.Anything) mockStatus.On("ReadyToMonitor") @@ -2273,6 +2281,8 @@ var _ = Describe("Testing core-controller installation", func() { mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("AddCertificateSigningRequests", mock.Anything) mockStatus.On("RemoveCertificateSigningRequests", mock.Anything) mockStatus.On("ReadyToMonitor") From a57b92a7fdfdbb1ecccaeaf2b3b6d6b3d65682dd Mon Sep 17 00:00:00 2001 From: Rene Dekker Date: Fri, 6 Mar 2026 12:02:36 -0800 Subject: [PATCH 5/7] Use static keys for certificate warnings to handle nil keypairs Ensures warnings are cleared even when a keypair becomes nil (e.g., Enterprise-only secrets on a Calico install), preventing stale warnings from lingering in TigeraStatus. Co-Authored-By: Claude Opus 4.6 --- .../installation/core_controller.go | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pkg/controller/installation/core_controller.go b/pkg/controller/installation/core_controller.go index 16ad6967b4..7047f66a10 100644 --- a/pkg/controller/installation/core_controller.go +++ b/pkg/controller/installation/core_controller.go @@ -1697,18 +1697,23 @@ func (r *ReconcileInstallation) Reconcile(ctx context.Context, request reconcile r.status.ReadyToMonitor() // Check BYO certificate expiry warnings and propagate them to the status manager. - for _, kp := range []certificatemanagement.KeyPairInterface{ - typhaNodeTLS.TyphaSecret, typhaNodeTLS.NodeSecret, typhaNodeTLS.TyphaSecretNonClusterHost, - nodePrometheusTLS, kubeControllerTLS, - } { - if kp == nil { - continue - } - if w := kp.Warnings(); w != "" { - r.status.SetWarning(kp.GetName(), w) - } else { - r.status.ClearWarning(kp.GetName()) + // Use a static key per slot so warnings are cleared even when a keypair becomes nil + // (e.g., when switching from Enterprise to Calico). + keyPairWarnings := map[string]certificatemanagement.KeyPairInterface{ + render.TyphaTLSSecretName: typhaNodeTLS.TyphaSecret, + render.NodeTLSSecretName: typhaNodeTLS.NodeSecret, + render.TyphaTLSSecretName + render.TyphaNonClusterHostSuffix: typhaNodeTLS.TyphaSecretNonClusterHost, + render.NodePrometheusTLSServerSecret: nodePrometheusTLS, + kubecontrollers.KubeControllerPrometheusTLSSecret: kubeControllerTLS, + } + for key, kp := range keyPairWarnings { + if kp != nil { + if w := kp.Warnings(); w != "" { + r.status.SetWarning(key, w) + continue + } } + r.status.ClearWarning(key) } // We can clear the degraded state now since as far as we know everything is in order. From b1db738735b65cdbcd542922998626098162044e Mon Sep 17 00:00:00 2001 From: Rene Dekker Date: Fri, 6 Mar 2026 15:19:58 -0800 Subject: [PATCH 6/7] Wire up BYO certificate expiry warnings in all controllers Add SetWarning/ClearWarning calls to 9 additional controllers (apiserver, authentication, compliance, intrusiondetection, logcollector, manager, monitor, packetcapture, policyrecommendation) and refactor status.go to use availableMessage() consistently across all condition types. Fix keypair_test.go warning message assertion to match implementation. Co-Authored-By: Claude Opus 4.6 --- .../apiserver/apiserver_controller.go | 18 +++++++++++++++- .../apiserver/apiserver_controller_test.go | 2 ++ .../authentication_controller.go | 13 ++++++++++++ .../authentication_controller_test.go | 4 ++++ .../compliance/compliance_controller.go | 17 +++++++++++++++ .../compliance/compliance_controller_test.go | 2 ++ .../intrusiondetection_controller.go | 17 ++++++++++++++- .../intrusiondetection_controller_test.go | 2 ++ .../logcollector/logcollector_controller.go | 14 +++++++++++++ .../logcollector_controller_test.go | 2 ++ pkg/controller/manager/manager_controller.go | 15 +++++++++++++ .../manager/manager_controller_test.go | 8 +++++++ pkg/controller/monitor/monitor_controller.go | 14 +++++++++++++ .../monitor/monitor_controller_test.go | 2 ++ .../packetcapture/packetcapture_controller.go | 13 ++++++++++++ .../packetcapture_controller_test.go | 2 ++ .../policyrecommendation_controller.go | 13 ++++++++++++ .../policyrecommendation_controller_test.go | 4 ++++ pkg/controller/status/status.go | 21 ++++++++++++------- pkg/tls/certificatemanagement/keypair.go | 2 +- pkg/tls/certificatemanagement/keypair_test.go | 2 +- 21 files changed, 175 insertions(+), 12 deletions(-) diff --git a/pkg/controller/apiserver/apiserver_controller.go b/pkg/controller/apiserver/apiserver_controller.go index 5151c738d7..b92193ab34 100644 --- a/pkg/controller/apiserver/apiserver_controller.go +++ b/pkg/controller/apiserver/apiserver_controller.go @@ -464,6 +464,7 @@ func (r *ReconcileAPIServer) Reconcile(ctx context.Context, request reconcile.Re var components []render.Component + var webhooksTLS certificatemanagement.KeyPairInterface certKeyPairOptions := []rcertificatemanagement.KeyPairOption{ rcertificatemanagement.NewKeyPairOption(tlsSecret, true, true), } @@ -481,7 +482,7 @@ func (r *ReconcileAPIServer) Reconcile(ctx context.Context, request reconcile.Re // // The network policy is included within the webhooks component so it is reconciled alongside // the Deployment. The TLS keypair is provisioned by the CertificateManagement component below. - webhooksTLS, err := certificateManager.GetOrCreateKeyPair( + webhooksTLS, err = certificateManager.GetOrCreateKeyPair( r.client, webhooks.WebhooksTLSSecretName, common.OperatorNamespace(), @@ -538,6 +539,21 @@ func (r *ReconcileAPIServer) Reconcile(ctx context.Context, request reconcile.Re } } + // Check BYO certificate expiry warnings. + for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + render.CalicoAPIServerTLSSecretName: tlsSecret, + "query-server-tls": queryServerTLSSecretCertificateManagementOnly, + webhooks.WebhooksTLSSecretName: webhooksTLS, + } { + if kp != nil { + if w := kp.Warnings(); w != "" { + r.status.SetWarning(key, w) + continue + } + } + r.status.ClearWarning(key) + } + // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/controller/apiserver/apiserver_controller_test.go b/pkg/controller/apiserver/apiserver_controller_test.go index dafa6867bb..2308c33461 100644 --- a/pkg/controller/apiserver/apiserver_controller_test.go +++ b/pkg/controller/apiserver/apiserver_controller_test.go @@ -147,6 +147,8 @@ var _ = Describe("apiserver controller tests", func() { mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("AddCertificateSigningRequests", mock.Anything) mockStatus.On("RemoveCertificateSigningRequests", mock.Anything) mockStatus.On("RemoveDeployments", mock.Anything) diff --git a/pkg/controller/authentication/authentication_controller.go b/pkg/controller/authentication/authentication_controller.go index 455ac386a5..cecb650b4f 100644 --- a/pkg/controller/authentication/authentication_controller.go +++ b/pkg/controller/authentication/authentication_controller.go @@ -429,6 +429,19 @@ func (r *ReconcileAuthentication) Reconcile(ctx context.Context, request reconci } } + // Check BYO certificate expiry warnings. + for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + render.DexTLSSecretName: tlsKeyPair, + } { + if kp != nil { + if w := kp.Warnings(); w != "" { + r.status.SetWarning(key, w) + continue + } + } + r.status.ClearWarning(key) + } + // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/controller/authentication/authentication_controller_test.go b/pkg/controller/authentication/authentication_controller_test.go index 18276efb08..63352a4881 100644 --- a/pkg/controller/authentication/authentication_controller_test.go +++ b/pkg/controller/authentication/authentication_controller_test.go @@ -86,6 +86,8 @@ var _ = Describe("authentication controller tests", func() { mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("SetDegraded", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return() mockStatus.On("ReadyToMonitor") mockStatus.On("OnCRNotFound").Return() @@ -489,6 +491,8 @@ var _ = Describe("authentication controller tests", func() { mockStatus = &status.MockStatus{} mockStatus.On("OnCRFound").Return() + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return().Maybe() + mockStatus.On("ClearWarning", mock.Anything).Return().Maybe() r = &ReconcileAuthentication{ client: cli, scheme: scheme, diff --git a/pkg/controller/compliance/compliance_controller.go b/pkg/controller/compliance/compliance_controller.go index 2e122b1ab7..2fc31ecf3e 100644 --- a/pkg/controller/compliance/compliance_controller.go +++ b/pkg/controller/compliance/compliance_controller.go @@ -512,6 +512,23 @@ func (r *ReconcileCompliance) Reconcile(ctx context.Context, request reconcile.R return reconcile.Result{}, nil } + // Check BYO certificate expiry warnings. + for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + render.ComplianceServerCertSecret: complianceServerKeyPair, + render.ComplianceSnapshotterSecret: snapshotterKeyPair.Interface, + render.ComplianceBenchmarkerSecret: benchmarkerKeyPair.Interface, + render.ComplianceReporterSecret: reporterKeyPair.Interface, + render.ComplianceControllerSecret: controllerKeyPair.Interface, + } { + if kp != nil { + if w := kp.Warnings(); w != "" { + r.status.SetWarning(key, w) + continue + } + } + r.status.ClearWarning(key) + } + // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/controller/compliance/compliance_controller_test.go b/pkg/controller/compliance/compliance_controller_test.go index b466365c19..090e6bdd9f 100644 --- a/pkg/controller/compliance/compliance_controller_test.go +++ b/pkg/controller/compliance/compliance_controller_test.go @@ -87,6 +87,8 @@ var _ = Describe("Compliance controller tests", func() { mockStatus.On("OnCRFound").Return() mockStatus.On("AddCertificateSigningRequests", mock.Anything).Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("ReadyToMonitor") mockStatus.On("SetMetaData", mock.Anything).Return() diff --git a/pkg/controller/intrusiondetection/intrusiondetection_controller.go b/pkg/controller/intrusiondetection/intrusiondetection_controller.go index 72017ebcca..68452a3be6 100644 --- a/pkg/controller/intrusiondetection/intrusiondetection_controller.go +++ b/pkg/controller/intrusiondetection/intrusiondetection_controller.go @@ -499,6 +499,7 @@ func (r *ReconcileIntrusionDetection) Reconcile(ctx context.Context, request rec intrusionDetectionComponent, } + var dpiKeyPair certificatemanagement.KeyPairInterface if !r.opts.MultiTenant { // FIXME: core controller creates TyphaNodeTLSConfig, this controller should only get it. // But changing the call from GetOrCreateTyphaNodeTLSConfig() to GetTyphaNodeTLSConfig() @@ -511,7 +512,7 @@ func (r *ReconcileIntrusionDetection) Reconcile(ctx context.Context, request rec typhaNodeTLS.TrustedBundle.AddCertificates(linseedCertificate) // dpiKeyPair is the key pair dpi presents to identify itself - dpiKeyPair, err := certificateManager.GetOrCreateKeyPair(r.client, render.DPITLSSecretName, helper.TruthNamespace(), []string{render.IntrusionDetectionTLSSecretName}) + dpiKeyPair, err = certificateManager.GetOrCreateKeyPair(r.client, render.DPITLSSecretName, helper.TruthNamespace(), []string{render.IntrusionDetectionTLSSecretName}) if err != nil { r.status.SetDegraded(operatorv1.ResourceCreateError, "Error creating TLS certificate", err, reqLogger) return reconcile.Result{}, err @@ -581,6 +582,20 @@ func (r *ReconcileIntrusionDetection) Reconcile(ctx context.Context, request rec return reconcile.Result{}, nil } + // Check BYO certificate expiry warnings. + for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + render.IntrusionDetectionTLSSecretName: intrusionDetectionKeyPair, + render.DPITLSSecretName: dpiKeyPair, + } { + if kp != nil { + if w := kp.Warnings(); w != "" { + r.status.SetWarning(key, w) + continue + } + } + r.status.ClearWarning(key) + } + // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/controller/intrusiondetection/intrusiondetection_controller_test.go b/pkg/controller/intrusiondetection/intrusiondetection_controller_test.go index 265f099122..1da34907cd 100644 --- a/pkg/controller/intrusiondetection/intrusiondetection_controller_test.go +++ b/pkg/controller/intrusiondetection/intrusiondetection_controller_test.go @@ -89,6 +89,8 @@ var _ = Describe("IntrusionDetection controller tests", func() { mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("SetDegraded", operatorv1.InvalidConfigurationError, mock.AnythingOfType("string"), mock.Anything, mock.Anything).Return().Maybe() mockStatus.On("SetDegraded", operatorv1.ResourceReadError, mock.AnythingOfType("string"), mock.Anything, mock.Anything).Return().Maybe() mockStatus.On("SetDegraded", operatorv1.ResourceUpdateError, mock.AnythingOfType("string"), mock.Anything, mock.Anything).Return().Maybe() diff --git a/pkg/controller/logcollector/logcollector_controller.go b/pkg/controller/logcollector/logcollector_controller.go index 31534b2f14..e8bb2c71c8 100644 --- a/pkg/controller/logcollector/logcollector_controller.go +++ b/pkg/controller/logcollector/logcollector_controller.go @@ -704,6 +704,20 @@ func (r *ReconcileLogCollector) Reconcile(ctx context.Context, request reconcile return reconcile.Result{}, nil } + // Check BYO certificate expiry warnings. + for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + render.FluentdPrometheusTLSSecretName: fluentdKeyPair, + render.EKSLogForwarderTLSSecretName: eksLogForwarderKeyPair, + } { + if kp != nil { + if w := kp.Warnings(); w != "" { + r.status.SetWarning(key, w) + continue + } + } + r.status.ClearWarning(key) + } + // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/controller/logcollector/logcollector_controller_test.go b/pkg/controller/logcollector/logcollector_controller_test.go index 47371ca82b..a82c20da43 100644 --- a/pkg/controller/logcollector/logcollector_controller_test.go +++ b/pkg/controller/logcollector/logcollector_controller_test.go @@ -81,6 +81,8 @@ var _ = Describe("LogCollector controller tests", func() { mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("SetDegraded", operatorv1.ResourceNotReady, "Waiting for LicenseKeyAPI to be ready", mock.Anything, mock.Anything).Return().Maybe() mockStatus.On("ReadyToMonitor") mockStatus.On("SetMetaData", mock.Anything).Return() diff --git a/pkg/controller/manager/manager_controller.go b/pkg/controller/manager/manager_controller.go index 01819f7bcb..34bedaa6b2 100644 --- a/pkg/controller/manager/manager_controller.go +++ b/pkg/controller/manager/manager_controller.go @@ -722,6 +722,21 @@ func (r *ReconcileManager) Reconcile(ctx context.Context, request reconcile.Requ } } + // Check BYO certificate expiry warnings. + for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + render.ManagerTLSSecretName: tlsSecret, + render.ManagerInternalTLSSecretName: internalTrafficSecret, + render.VoltronLinseedTLS: linseedVoltronServerCert, + } { + if kp != nil { + if w := kp.Warnings(); w != "" { + r.status.SetWarning(key, w) + continue + } + } + r.status.ClearWarning(key) + } + // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() instance.Status.State = operatorv1.TigeraStatusReady diff --git a/pkg/controller/manager/manager_controller_test.go b/pkg/controller/manager/manager_controller_test.go index 122ed69e79..1a0b2487e4 100644 --- a/pkg/controller/manager/manager_controller_test.go +++ b/pkg/controller/manager/manager_controller_test.go @@ -146,6 +146,8 @@ var _ = Describe("Manager controller tests", func() { mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("SetDegraded", operatorv1.ResourceNotReady, "Waiting for LicenseKeyAPI to be ready", mock.Anything, mock.Anything).Return().Maybe() mockStatus.On("SetDegraded", operatorv1.ResourceNotReady, "Waiting for secret 'tigera-packetcapture-server-tls' to become available", mock.Anything, mock.Anything).Return().Maybe() mockStatus.On("SetDegraded", operatorv1.ResourceNotReady, "Waiting for secret 'tigera-secure-linseed-cert' to become available", mock.Anything, mock.Anything).Return().Maybe() @@ -525,6 +527,8 @@ var _ = Describe("Manager controller tests", func() { mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("SetDegraded", operatorv1.ResourceNotReady, "Waiting for LicenseKeyAPI to be ready", mock.Anything, mock.Anything).Return().Maybe() mockStatus.On("SetDegraded", operatorv1.ResourceNotReady, "Waiting for secret 'calico-node-prometheus-tls' to become available", mock.Anything, mock.Anything).Return().Maybe() mockStatus.On("SetDegraded", operatorv1.ResourceNotReady, "Waiting for secret 'tigera-packetcapture-server-tls' to become available", mock.Anything, mock.Anything).Return().Maybe() @@ -763,6 +767,8 @@ var _ = Describe("Manager controller tests", func() { mockStatus.On("AddDeployments", mock.Anything) mockStatus.On("RemoveDeployments", []types.NamespacedName{{Name: render.LegacyManagerDeploymentName, Namespace: render.LegacyManagerNamespace}}).Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return().Maybe() + mockStatus.On("ClearWarning", mock.Anything).Return().Maybe() mockStatus.On("SetDegraded", operatorv1.ResourceNotReady, "Compliance is not ready", mock.Anything, mock.Anything).Return().Maybe() mockStatus.On("RemoveCertificateSigningRequests", mock.Anything) mockStatus.On("ReadyToMonitor") @@ -1192,6 +1198,8 @@ var _ = Describe("Manager controller tests", func() { mockStatus.On("RemoveDeployments", []types.NamespacedName{{Name: render.LegacyManagerDeploymentName, Namespace: tenantBNamespace}}).Return() mockStatus.On("ReadyToMonitor") mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("IsAvailable").Return(true) r.opts.MultiTenant = true diff --git a/pkg/controller/monitor/monitor_controller.go b/pkg/controller/monitor/monitor_controller.go index 93dbc3ea56..5c459b7fdd 100644 --- a/pkg/controller/monitor/monitor_controller.go +++ b/pkg/controller/monitor/monitor_controller.go @@ -471,6 +471,20 @@ func (r *ReconcileMonitor) Reconcile(ctx context.Context, request reconcile.Requ return reconcile.Result{}, nil } + // Check BYO certificate expiry warnings. + for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + monitor.PrometheusServerTLSSecretName: serverTLSSecret, + monitor.PrometheusClientTLSSecretName: clientTLSSecret, + } { + if kp != nil { + if w := kp.Warnings(); w != "" { + r.status.SetWarning(key, w) + continue + } + } + r.status.ClearWarning(key) + } + r.status.ClearDegraded() if !r.status.IsAvailable() { diff --git a/pkg/controller/monitor/monitor_controller_test.go b/pkg/controller/monitor/monitor_controller_test.go index 9c8df546c3..1265840e67 100644 --- a/pkg/controller/monitor/monitor_controller_test.go +++ b/pkg/controller/monitor/monitor_controller_test.go @@ -82,6 +82,8 @@ var _ = Describe("Monitor controller tests", func() { mockStatus.On("AddDeployments", mock.Anything).Return() mockStatus.On("AddStatefulSets", mock.Anything) mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ReadyToMonitor") diff --git a/pkg/controller/packetcapture/packetcapture_controller.go b/pkg/controller/packetcapture/packetcapture_controller.go index 5aad77c761..a725d7a3c5 100644 --- a/pkg/controller/packetcapture/packetcapture_controller.go +++ b/pkg/controller/packetcapture/packetcapture_controller.go @@ -303,6 +303,19 @@ func (r *ReconcilePacketCapture) Reconcile(ctx context.Context, request reconcil } } + // Check BYO certificate expiry warnings. + for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + render.PacketCaptureServerCert: packetCaptureCertSecret, + } { + if kp != nil { + if w := kp.Warnings(); w != "" { + r.status.SetWarning(key, w) + continue + } + } + r.status.ClearWarning(key) + } + // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/controller/packetcapture/packetcapture_controller_test.go b/pkg/controller/packetcapture/packetcapture_controller_test.go index 74145ce2d8..246553f190 100644 --- a/pkg/controller/packetcapture/packetcapture_controller_test.go +++ b/pkg/controller/packetcapture/packetcapture_controller_test.go @@ -139,6 +139,8 @@ var _ = Describe("packet capture controller tests", func() { mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("ReadyToMonitor") mockStatus.On("SetMetaData", mock.Anything).Return() mockStatus.On("SetDegraded", operatorv1.ResourceValidationError, mock.AnythingOfType("string"), mock.Anything, mock.Anything).Return().Maybe() diff --git a/pkg/controller/policyrecommendation/policyrecommendation_controller.go b/pkg/controller/policyrecommendation/policyrecommendation_controller.go index d22ec45557..49c123f41d 100644 --- a/pkg/controller/policyrecommendation/policyrecommendation_controller.go +++ b/pkg/controller/policyrecommendation/policyrecommendation_controller.go @@ -474,6 +474,19 @@ func (r *ReconcilePolicyRecommendation) Reconcile(ctx context.Context, request r } } + // Check BYO certificate expiry warnings. + for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + render.PolicyRecommendationTLSSecretName: policyRecommendationKeyPair, + } { + if kp != nil { + if w := kp.Warnings(); w != "" { + r.status.SetWarning(key, w) + continue + } + } + r.status.ClearWarning(key) + } + // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/controller/policyrecommendation/policyrecommendation_controller_test.go b/pkg/controller/policyrecommendation/policyrecommendation_controller_test.go index ebf0746b42..4189337bb6 100644 --- a/pkg/controller/policyrecommendation/policyrecommendation_controller_test.go +++ b/pkg/controller/policyrecommendation/policyrecommendation_controller_test.go @@ -83,6 +83,8 @@ var _ = Describe("PolicyRecommendation controller tests", func() { mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("SetDegraded", operatorv1.ResourceValidationError, mock.AnythingOfType("string"), mock.Anything, mock.Anything).Return().Maybe() mockStatus.On("SetDegraded", operatorv1.ResourceReadError, mock.AnythingOfType("string"), mock.Anything, mock.Anything).Return().Maybe() mockStatus.On("SetDegraded", operatorv1.ResourceUpdateError, mock.AnythingOfType("string"), mock.Anything, mock.Anything).Return().Maybe() @@ -495,6 +497,8 @@ var _ = Describe("PolicyRecommendation controller tests", func() { mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() mockStatus.On("ClearDegraded") + mockStatus.On("SetWarning", mock.Anything, mock.Anything).Return() + mockStatus.On("ClearWarning", mock.Anything).Return() mockStatus.On("SetDegraded", operatorv1.ResourceValidationError, mock.AnythingOfType("string"), mock.Anything, mock.Anything).Return().Maybe() mockStatus.On("SetDegraded", operatorv1.ResourceReadError, mock.AnythingOfType("string"), mock.Anything, mock.Anything).Return().Maybe() mockStatus.On("SetDegraded", operatorv1.ResourceUpdateError, mock.AnythingOfType("string"), mock.Anything, mock.Anything).Return().Maybe() diff --git a/pkg/controller/status/status.go b/pkg/controller/status/status.go index 86a8c598ff..1a9e00650d 100644 --- a/pkg/controller/status/status.go +++ b/pkg/controller/status/status.go @@ -167,12 +167,9 @@ func (m *statusManager) updateStatus() { // We've collected knowledge about the current state of the objects we're monitoring. // Now, use that to update the TigeraStatus object for this manager. available := m.IsAvailable() - if m.IsAvailable() { - msg := "All objects available" - if w := m.warningMessage(); w != "" { - msg = msg + "; " + w - } - m.setAvailable(operator.AllObjectsAvailable, msg) + availableMsg := m.availableMessage() + if available { + m.setAvailable(operator.AllObjectsAvailable, availableMsg) } else { m.clearAvailable() } @@ -181,7 +178,7 @@ func (m *statusManager) updateStatus() { m.setProgressing(operator.ResourceNotReady, m.progressingMessage()) } else { if available { - m.clearProgressingWithReason(operator.AllObjectsAvailable, "All Objects Available") + m.clearProgressingWithReason(operator.AllObjectsAvailable, availableMsg) } else { m.clearProgressing() } @@ -191,7 +188,7 @@ func (m *statusManager) updateStatus() { m.setDegraded(m.degradedReason(), m.degradedMessage()) } else { if available { - m.clearDegradedWithReason(operator.AllObjectsAvailable, "All Objects Available") + m.clearDegradedWithReason(operator.AllObjectsAvailable, availableMsg) } else { m.clearDegraded() } @@ -848,6 +845,14 @@ func (m *statusManager) clearAvailable() { m.set(true, conditions...) } +func (m *statusManager) availableMessage() string { + msg := "All objects available" + if w := m.warningMessage(); w != "" { + msg = msg + "; " + w + } + return msg +} + func (m *statusManager) progressingMessage() string { m.lock.Lock() defer m.lock.Unlock() diff --git a/pkg/tls/certificatemanagement/keypair.go b/pkg/tls/certificatemanagement/keypair.go index 38121722b6..8736c94e4e 100644 --- a/pkg/tls/certificatemanagement/keypair.go +++ b/pkg/tls/certificatemanagement/keypair.go @@ -125,7 +125,7 @@ func (k *KeyPair) Warnings() string { } remaining := time.Until(cert.NotAfter) if remaining <= 30*24*time.Hour { - return fmt.Sprintf("BYO certificate %q expires in %d days", k.Name, int(remaining.Hours()/24)) + return fmt.Sprintf("Warning: user provided certificate %q expires in %d days", k.Name, int(remaining.Hours()/24)) } return "" } diff --git a/pkg/tls/certificatemanagement/keypair_test.go b/pkg/tls/certificatemanagement/keypair_test.go index 262e87c04f..9cbeba8134 100644 --- a/pkg/tls/certificatemanagement/keypair_test.go +++ b/pkg/tls/certificatemanagement/keypair_test.go @@ -114,7 +114,7 @@ var _ = Describe("TLS secret metadata", func() { } Expect(kp.BYO()).To(BeTrue()) warning := kp.Warnings() - Expect(warning).To(ContainSubstring("BYO certificate")) + Expect(warning).To(ContainSubstring("user provided certificate")) Expect(warning).To(ContainSubstring("my-tls-secret")) Expect(warning).To(ContainSubstring("expires in")) }) From be988c6289a11e6a2f58c26178c113e6d4aabdf1 Mon Sep 17 00:00:00 2001 From: Rene Dekker Date: Fri, 6 Mar 2026 16:10:34 -0800 Subject: [PATCH 7/7] Address PR review: extract CheckKeyPairWarnings helper and isOperatorManaged - Add CheckKeyPairWarnings() helper in certificatemanagement package to deduplicate the warning check loop across all controllers - Add WarningReporter interface to avoid circular dependency with status pkg - Extract isOperatorManaged() helper in MergeMaps to consolidate operator annotation filtering (covers all operator.tigera.io annotations) Co-Authored-By: Claude Opus 4.6 --- pkg/common/components.go | 12 +++++++---- .../apiserver/apiserver_controller.go | 13 ++---------- .../authentication_controller.go | 12 ++--------- .../compliance/compliance_controller.go | 12 ++--------- .../installation/core_controller.go | 15 ++------------ .../intrusiondetection_controller.go | 12 ++--------- .../logcollector/logcollector_controller.go | 12 ++--------- pkg/controller/manager/manager_controller.go | 12 ++--------- pkg/controller/monitor/monitor_controller.go | 12 ++--------- .../packetcapture/packetcapture_controller.go | 12 ++--------- .../policyrecommendation_controller.go | 12 ++--------- pkg/tls/certificatemanagement/keypair.go | 20 +++++++++++++++++++ 12 files changed, 48 insertions(+), 108 deletions(-) diff --git a/pkg/common/components.go b/pkg/common/components.go index f021d50c6b..4597534278 100644 --- a/pkg/common/components.go +++ b/pkg/common/components.go @@ -22,18 +22,22 @@ import ( // MergeMaps merges current and desired maps. If both current and desired maps contain the same key, the // desired map's value is used. -// MergeMaps does not copy hash.operator.tigera.io or certificates.operator.tigera.io annotations from the -// current map, since those are managed by the operator. +// MergeMaps does not copy operator-managed annotations from the current map. func MergeMaps(current, desired map[string]string) map[string]string { for k, v := range current { - // Copy over key/value that should be copied. - if _, ok := desired[k]; !ok && !strings.Contains(k, "hash.operator.tigera.io") && !strings.Contains(k, "certificates.operator.tigera.io") { + if _, ok := desired[k]; !ok && !isOperatorManaged(k) { desired[k] = v } } return desired } +// isOperatorManaged returns true if the given annotation key is managed by the operator +// and should not be copied from the current map during merges. +func isOperatorManaged(key string) bool { + return strings.Contains(key, "operator.tigera.io") +} + // MapExistsOrInitialize returns the given map if non-nil or returns an empty map. func MapExistsOrInitialize(m map[string]string) map[string]string { if m != nil { diff --git a/pkg/controller/apiserver/apiserver_controller.go b/pkg/controller/apiserver/apiserver_controller.go index b92193ab34..c56aa15276 100644 --- a/pkg/controller/apiserver/apiserver_controller.go +++ b/pkg/controller/apiserver/apiserver_controller.go @@ -540,19 +540,10 @@ func (r *ReconcileAPIServer) Reconcile(ctx context.Context, request reconcile.Re } // Check BYO certificate expiry warnings. - for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + certificatemanagement.CheckKeyPairWarnings(map[string]certificatemanagement.KeyPairInterface{ render.CalicoAPIServerTLSSecretName: tlsSecret, - "query-server-tls": queryServerTLSSecretCertificateManagementOnly, webhooks.WebhooksTLSSecretName: webhooksTLS, - } { - if kp != nil { - if w := kp.Warnings(); w != "" { - r.status.SetWarning(key, w) - continue - } - } - r.status.ClearWarning(key) - } + }, r.status) // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/controller/authentication/authentication_controller.go b/pkg/controller/authentication/authentication_controller.go index cecb650b4f..7c91d0b861 100644 --- a/pkg/controller/authentication/authentication_controller.go +++ b/pkg/controller/authentication/authentication_controller.go @@ -430,17 +430,9 @@ func (r *ReconcileAuthentication) Reconcile(ctx context.Context, request reconci } // Check BYO certificate expiry warnings. - for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + certificatemanagement.CheckKeyPairWarnings(map[string]certificatemanagement.KeyPairInterface{ render.DexTLSSecretName: tlsKeyPair, - } { - if kp != nil { - if w := kp.Warnings(); w != "" { - r.status.SetWarning(key, w) - continue - } - } - r.status.ClearWarning(key) - } + }, r.status) // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/controller/compliance/compliance_controller.go b/pkg/controller/compliance/compliance_controller.go index 2fc31ecf3e..27894db556 100644 --- a/pkg/controller/compliance/compliance_controller.go +++ b/pkg/controller/compliance/compliance_controller.go @@ -513,21 +513,13 @@ func (r *ReconcileCompliance) Reconcile(ctx context.Context, request reconcile.R } // Check BYO certificate expiry warnings. - for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + certificatemanagement.CheckKeyPairWarnings(map[string]certificatemanagement.KeyPairInterface{ render.ComplianceServerCertSecret: complianceServerKeyPair, render.ComplianceSnapshotterSecret: snapshotterKeyPair.Interface, render.ComplianceBenchmarkerSecret: benchmarkerKeyPair.Interface, render.ComplianceReporterSecret: reporterKeyPair.Interface, render.ComplianceControllerSecret: controllerKeyPair.Interface, - } { - if kp != nil { - if w := kp.Warnings(); w != "" { - r.status.SetWarning(key, w) - continue - } - } - r.status.ClearWarning(key) - } + }, r.status) // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/controller/installation/core_controller.go b/pkg/controller/installation/core_controller.go index 7047f66a10..1e302099fe 100644 --- a/pkg/controller/installation/core_controller.go +++ b/pkg/controller/installation/core_controller.go @@ -1697,24 +1697,13 @@ func (r *ReconcileInstallation) Reconcile(ctx context.Context, request reconcile r.status.ReadyToMonitor() // Check BYO certificate expiry warnings and propagate them to the status manager. - // Use a static key per slot so warnings are cleared even when a keypair becomes nil - // (e.g., when switching from Enterprise to Calico). - keyPairWarnings := map[string]certificatemanagement.KeyPairInterface{ + certificatemanagement.CheckKeyPairWarnings(map[string]certificatemanagement.KeyPairInterface{ render.TyphaTLSSecretName: typhaNodeTLS.TyphaSecret, render.NodeTLSSecretName: typhaNodeTLS.NodeSecret, render.TyphaTLSSecretName + render.TyphaNonClusterHostSuffix: typhaNodeTLS.TyphaSecretNonClusterHost, render.NodePrometheusTLSServerSecret: nodePrometheusTLS, kubecontrollers.KubeControllerPrometheusTLSSecret: kubeControllerTLS, - } - for key, kp := range keyPairWarnings { - if kp != nil { - if w := kp.Warnings(); w != "" { - r.status.SetWarning(key, w) - continue - } - } - r.status.ClearWarning(key) - } + }, r.status) // We can clear the degraded state now since as far as we know everything is in order. r.status.ClearDegraded() diff --git a/pkg/controller/intrusiondetection/intrusiondetection_controller.go b/pkg/controller/intrusiondetection/intrusiondetection_controller.go index 68452a3be6..0904ea4d30 100644 --- a/pkg/controller/intrusiondetection/intrusiondetection_controller.go +++ b/pkg/controller/intrusiondetection/intrusiondetection_controller.go @@ -583,18 +583,10 @@ func (r *ReconcileIntrusionDetection) Reconcile(ctx context.Context, request rec } // Check BYO certificate expiry warnings. - for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + certificatemanagement.CheckKeyPairWarnings(map[string]certificatemanagement.KeyPairInterface{ render.IntrusionDetectionTLSSecretName: intrusionDetectionKeyPair, render.DPITLSSecretName: dpiKeyPair, - } { - if kp != nil { - if w := kp.Warnings(); w != "" { - r.status.SetWarning(key, w) - continue - } - } - r.status.ClearWarning(key) - } + }, r.status) // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/controller/logcollector/logcollector_controller.go b/pkg/controller/logcollector/logcollector_controller.go index e8bb2c71c8..3a95d83d40 100644 --- a/pkg/controller/logcollector/logcollector_controller.go +++ b/pkg/controller/logcollector/logcollector_controller.go @@ -705,18 +705,10 @@ func (r *ReconcileLogCollector) Reconcile(ctx context.Context, request reconcile } // Check BYO certificate expiry warnings. - for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + certificatemanagement.CheckKeyPairWarnings(map[string]certificatemanagement.KeyPairInterface{ render.FluentdPrometheusTLSSecretName: fluentdKeyPair, render.EKSLogForwarderTLSSecretName: eksLogForwarderKeyPair, - } { - if kp != nil { - if w := kp.Warnings(); w != "" { - r.status.SetWarning(key, w) - continue - } - } - r.status.ClearWarning(key) - } + }, r.status) // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/controller/manager/manager_controller.go b/pkg/controller/manager/manager_controller.go index 34bedaa6b2..c3c4958426 100644 --- a/pkg/controller/manager/manager_controller.go +++ b/pkg/controller/manager/manager_controller.go @@ -723,19 +723,11 @@ func (r *ReconcileManager) Reconcile(ctx context.Context, request reconcile.Requ } // Check BYO certificate expiry warnings. - for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + certificatemanagement.CheckKeyPairWarnings(map[string]certificatemanagement.KeyPairInterface{ render.ManagerTLSSecretName: tlsSecret, render.ManagerInternalTLSSecretName: internalTrafficSecret, render.VoltronLinseedTLS: linseedVoltronServerCert, - } { - if kp != nil { - if w := kp.Warnings(); w != "" { - r.status.SetWarning(key, w) - continue - } - } - r.status.ClearWarning(key) - } + }, r.status) // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/controller/monitor/monitor_controller.go b/pkg/controller/monitor/monitor_controller.go index 5c459b7fdd..123e27d8bf 100644 --- a/pkg/controller/monitor/monitor_controller.go +++ b/pkg/controller/monitor/monitor_controller.go @@ -472,18 +472,10 @@ func (r *ReconcileMonitor) Reconcile(ctx context.Context, request reconcile.Requ } // Check BYO certificate expiry warnings. - for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + certificatemanagement.CheckKeyPairWarnings(map[string]certificatemanagement.KeyPairInterface{ monitor.PrometheusServerTLSSecretName: serverTLSSecret, monitor.PrometheusClientTLSSecretName: clientTLSSecret, - } { - if kp != nil { - if w := kp.Warnings(); w != "" { - r.status.SetWarning(key, w) - continue - } - } - r.status.ClearWarning(key) - } + }, r.status) r.status.ClearDegraded() diff --git a/pkg/controller/packetcapture/packetcapture_controller.go b/pkg/controller/packetcapture/packetcapture_controller.go index a725d7a3c5..1ffeb4c151 100644 --- a/pkg/controller/packetcapture/packetcapture_controller.go +++ b/pkg/controller/packetcapture/packetcapture_controller.go @@ -304,17 +304,9 @@ func (r *ReconcilePacketCapture) Reconcile(ctx context.Context, request reconcil } // Check BYO certificate expiry warnings. - for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + certificatemanagement.CheckKeyPairWarnings(map[string]certificatemanagement.KeyPairInterface{ render.PacketCaptureServerCert: packetCaptureCertSecret, - } { - if kp != nil { - if w := kp.Warnings(); w != "" { - r.status.SetWarning(key, w) - continue - } - } - r.status.ClearWarning(key) - } + }, r.status) // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/controller/policyrecommendation/policyrecommendation_controller.go b/pkg/controller/policyrecommendation/policyrecommendation_controller.go index 49c123f41d..3bfa067782 100644 --- a/pkg/controller/policyrecommendation/policyrecommendation_controller.go +++ b/pkg/controller/policyrecommendation/policyrecommendation_controller.go @@ -475,17 +475,9 @@ func (r *ReconcilePolicyRecommendation) Reconcile(ctx context.Context, request r } // Check BYO certificate expiry warnings. - for key, kp := range map[string]certificatemanagement.KeyPairInterface{ + certificatemanagement.CheckKeyPairWarnings(map[string]certificatemanagement.KeyPairInterface{ render.PolicyRecommendationTLSSecretName: policyRecommendationKeyPair, - } { - if kp != nil { - if w := kp.Warnings(); w != "" { - r.status.SetWarning(key, w) - continue - } - } - r.status.ClearWarning(key) - } + }, r.status) // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() diff --git a/pkg/tls/certificatemanagement/keypair.go b/pkg/tls/certificatemanagement/keypair.go index 8736c94e4e..b42b3957d1 100644 --- a/pkg/tls/certificatemanagement/keypair.go +++ b/pkg/tls/certificatemanagement/keypair.go @@ -274,6 +274,26 @@ func GetKeyCertPEM(secret *corev1.Secret) ([]byte, []byte) { return nil, nil } +// WarningReporter is a minimal interface for reporting certificate warnings to a status manager. +type WarningReporter interface { + SetWarning(key string, msg string) + ClearWarning(key string) +} + +// CheckKeyPairWarnings checks each keypair for BYO certificate expiry warnings and reports them +// to the status manager. For nil keypairs or keypairs without warnings, the warning is cleared. +func CheckKeyPairWarnings(keyPairs map[string]KeyPairInterface, status WarningReporter) { + for key, kp := range keyPairs { + if kp != nil { + if w := kp.Warnings(); w != "" { + status.SetWarning(key, w) + continue + } + } + status.ClearWarning(key) + } +} + // NewKeyPair returns a KeyPair, which wraps a Secret object that contains a private key and a certificate. Whether certificate // management is configured or not, KeyPair returns the right InitContainer, Volumemount or Volume (when applicable). func NewKeyPair(secret *corev1.Secret, dnsNames []string, clusterDomain string) KeyPairInterface {