diff --git a/go.mod b/go.mod index b77e042..21c34ef 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,12 @@ require ( github.com/go-logr/logr v1.4.3 github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.1 - github.com/openshift/api v0.0.0-20260213155647-8fe9fe363807 + github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb github.com/openshift/library-go v0.0.0-20260213153706-03f1709971c5 - k8s.io/apimachinery v0.35.1 - k8s.io/client-go v0.35.1 - k8s.io/utils v0.0.0-20260108192941-914a6e750570 - sigs.k8s.io/controller-runtime v0.23.2 + k8s.io/apimachinery v0.35.2 + k8s.io/client-go v0.35.2 + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 + sigs.k8s.io/controller-runtime v0.23.3 ) require ( @@ -64,7 +64,7 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.35.1 // indirect + k8s.io/api v0.35.2 // indirect k8s.io/apiextensions-apiserver v0.35.1 // indirect k8s.io/apiserver v0.35.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect diff --git a/go.sum b/go.sum index b54e5c5..e7b8926 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,8 @@ github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= -github.com/openshift/api v0.0.0-20260213155647-8fe9fe363807 h1:coR/haF16EW8KS1E/PwJfDzMSy4mU9K0H1rcHejqYDY= -github.com/openshift/api v0.0.0-20260213155647-8fe9fe363807/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY= +github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb h1:iwBR3mzmyE3EMFx7R3CQ9lOccTS0dNht8TW82aGITg0= +github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo= github.com/openshift/library-go v0.0.0-20260213153706-03f1709971c5 h1:9Pe6iVOMjt9CdA/vaKBNUSoEIjIe1po5Ha3ABRYXLJI= github.com/openshift/library-go v0.0.0-20260213153706-03f1709971c5/go.mod h1:K3FoNLgNBFYbFuG+Kr8usAnQxj1w84XogyUp2M8rK8k= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -173,24 +173,24 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= -k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= +k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= -k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= -k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= +k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs= k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4= -k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= -k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= +k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY= -k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -sigs.k8s.io/controller-runtime v0.23.2 h1:Oh3FliXaA2CS1chpUXvjVNJtsvGZYUxQH8s7bvR7aXk= -sigs.k8s.io/controller-runtime v0.23.2/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= diff --git a/pkg/tls/controller.go b/pkg/tls/controller.go index b7efbd9..788634f 100644 --- a/pkg/tls/controller.go +++ b/pkg/tls/controller.go @@ -41,6 +41,9 @@ type SecurityProfileWatcher struct { // InitialTLSProfileSpec is the TLS profile spec that was configured when the operator started. InitialTLSProfileSpec configv1.TLSProfileSpec + // InitialTLSAdherencePolicy is the TLS adherence policy that was configured when the operator started. + InitialTLSAdherencePolicy configv1.TLSAdherencePolicy + // OnProfileChange is a function that will be called when the TLS profile changes. // It receives the reconcile context, old and new TLS profile specs. // This allows the caller to make decisions based on the actual profile changes. @@ -66,6 +69,9 @@ type SecurityProfileWatcher struct { // }, // } OnProfileChange func(ctx context.Context, oldTLSProfileSpec, newTLSProfileSpec configv1.TLSProfileSpec) + + // OnAdherencePolicyChange is a function that will be called when the TLS adherence policy changes. + OnAdherencePolicyChange func(ctx context.Context, oldTLSAdherencePolicy, newTLSAdherencePolicy configv1.TLSAdherencePolicy) } // SetupWithManager sets up the controller with the Manager. @@ -139,6 +145,17 @@ func (r *SecurityProfileWatcher) Reconcile(ctx context.Context, req ctrl.Request r.InitialTLSProfileSpec = currentTLSProfileSpec } + // Compare the current TLS adherence policy with the initial one. + if tlsAdherencePolicyChanged := r.InitialTLSAdherencePolicy != apiServer.Spec.TLSAdherence; tlsAdherencePolicyChanged { + // TLS adherence policy has changed, invoke the callback if it is set. + if r.OnAdherencePolicyChange != nil { + r.OnAdherencePolicyChange(ctx, r.InitialTLSAdherencePolicy, apiServer.Spec.TLSAdherence) + } + + // Persist the new adherence policy for future change detection. + r.InitialTLSAdherencePolicy = apiServer.Spec.TLSAdherence + } + // No need to requeue, as the callback will handle further actions. return ctrl.Result{}, nil } diff --git a/pkg/tls/controller_test.go b/pkg/tls/controller_test.go index 04005ff..a2897a1 100644 --- a/pkg/tls/controller_test.go +++ b/pkg/tls/controller_test.go @@ -58,12 +58,18 @@ var _ = Describe("SecurityProfileWatcher controller", func() { new configv1.TLSProfileSpec } + type adherencePolicyChange struct { + old configv1.TLSAdherencePolicy + new configv1.TLSAdherencePolicy + } + var ( - mgrCancel context.CancelFunc - mgrDone chan struct{} - mgr manager.Manager - apiServer *configv1.APIServer - profileChanges *atomicSlice[profileChange] + mgrCancel context.CancelFunc + mgrDone chan struct{} + mgr manager.Manager + apiServer *configv1.APIServer + profileChanges *atomicSlice[profileChange] + adherencePolicyChanges *atomicSlice[adherencePolicyChange] ) BeforeEach(func() { @@ -88,6 +94,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Reset callback tracking. profileChanges = &atomicSlice[profileChange]{} + adherencePolicyChanges = &atomicSlice[adherencePolicyChange]{} }) AfterEach(func() { @@ -101,18 +108,22 @@ var _ = Describe("SecurityProfileWatcher controller", func() { Expect(k8sClient.Delete(ctx, apiServer)).To(Succeed()) }) - startManager := func(initialProfile configv1.TLSProfileSpec) { + startManager := func(initialProfile configv1.TLSProfileSpec, initialAdherencePolicy configv1.TLSAdherencePolicy) { var mgrCtx context.Context mgrCtx, mgrCancel = context.WithCancel(ctx) mgrDone = make(chan struct{}) // Set up the TLS security profile watcher controller. watcher := &SecurityProfileWatcher{ - Client: mgr.GetClient(), - InitialTLSProfileSpec: initialProfile, + Client: mgr.GetClient(), + InitialTLSProfileSpec: initialProfile, + InitialTLSAdherencePolicy: initialAdherencePolicy, OnProfileChange: func(_ context.Context, oldSpec, newSpec configv1.TLSProfileSpec) { profileChanges.Append(profileChange{old: oldSpec, new: newSpec}) }, + OnAdherencePolicyChange: func(_ context.Context, oldPolicy, newPolicy configv1.TLSAdherencePolicy) { + adherencePolicyChanges.Append(adherencePolicyChange{old: oldPolicy, new: newPolicy}) + }, } Expect(watcher.SetupWithManager(mgr)).To(Succeed()) @@ -135,7 +146,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the intermediate profile (same as what's configured). initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Wait a bit and verify callback was not invoked. Consistently(profileChanges.Len).Should(Equal(0), "callback should not be invoked") @@ -145,7 +156,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the intermediate profile. initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Get the intermediate profile spec to replicate it exactly. intermediateSpec := *configv1.TLSProfiles[configv1.TLSProfileIntermediateType] @@ -179,7 +190,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the custom profile. initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Switch to the intermediate profile (which has identical settings). apiServer.Spec.TLSSecurityProfile = &configv1.TLSSecurityProfile{ @@ -197,7 +208,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the intermediate profile. initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Update the APIServer to use the Modern profile (which has TLS 1.3). apiServer.Spec.TLSSecurityProfile = &configv1.TLSSecurityProfile{ @@ -220,7 +231,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the intermediate profile. initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Define the custom profile we'll switch to. customSpec := configv1.TLSProfileSpec{ @@ -262,7 +273,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the custom profile. initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Switch back to the intermediate profile. apiServer.Spec.TLSSecurityProfile = &configv1.TLSSecurityProfile{ @@ -278,7 +289,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the intermediate profile (profile A). initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Change from A (Intermediate) to B (Modern). apiServer.Spec.TLSSecurityProfile = &configv1.TLSSecurityProfile{ @@ -325,7 +336,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the default (nil -> intermediate) profile. initialProfile, err := GetTLSProfileSpec(nil) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Update the APIServer to use the Modern profile. apiServer.Spec.TLSSecurityProfile = &configv1.TLSSecurityProfile{ @@ -337,4 +348,25 @@ var _ = Describe("SecurityProfileWatcher controller", func() { Eventually(profileChanges.Len).Should(Equal(1), "callback should be invoked once") }) }) + + Context("when the TLS adherence policy changes", func() { + It("should invoke the callback when policy changes", func() { + // Start with the intermediate profile. + initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) + Expect(err).NotTo(HaveOccurred()) + startManager(initialProfile, apiServer.Spec.TLSAdherence) + + // Update the APIServer to use a different adherence policy. + apiServer.Spec.TLSAdherence = configv1.TLSAdherencePolicyStrictAllComponents + Expect(k8sClient.Update(ctx, apiServer)).To(Succeed()) + + // Verify callback was invoked. + Eventually(adherencePolicyChanges.Len).Should(Equal(1), "callback should be invoked once") + + // Verify the callback received the correct policies. + change := adherencePolicyChanges.Index(0) + Expect(change.old).To(Equal(configv1.TLSAdherencePolicyNoOpinion), "callback should receive the initial policy as old") + Expect(change.new).To(Equal(configv1.TLSAdherencePolicyStrictAllComponents), "callback should receive the current policy as new") + }) + }) }) diff --git a/pkg/tls/tls.go b/pkg/tls/tls.go index 6b33bd1..ce1e8c7 100644 --- a/pkg/tls/tls.go +++ b/pkg/tls/tls.go @@ -61,6 +61,19 @@ func FetchAPIServerTLSProfile(ctx context.Context, k8sClient client.Client) (con return profile, nil } +// FetchAPIServerTLSAdherencePolicy fetches the TLS adherence policy configured in APIServer. +// If no policy is configured, the default policy is returned. +func FetchAPIServerTLSAdherencePolicy(ctx context.Context, k8sClient client.Client) (configv1.TLSAdherencePolicy, error) { + apiServer := &configv1.APIServer{} + key := client.ObjectKey{Name: APIServerName} + + if err := k8sClient.Get(ctx, key, apiServer); err != nil { + return configv1.TLSAdherencePolicyNoOpinion, fmt.Errorf("failed to get APIServer %q: %w", key.String(), err) + } + + return apiServer.Spec.TLSAdherence, nil +} + // GetTLSProfileSpec returns TLSProfileSpec for the given profile. // If no profile is configured, the default profile is returned. func GetTLSProfileSpec(profile *configv1.TLSSecurityProfile) (configv1.TLSProfileSpec, error) {