88 "fmt"
99 "net"
1010 "net/http"
11+ "slices"
1112 "time"
1213
1314 "github.com/prometheus/client_golang/prometheus"
@@ -27,6 +28,8 @@ import (
2728 "k8s.io/klog/v2"
2829
2930 configv1 "github.com/openshift/api/config/v1"
31+ configlistersv1 "github.com/openshift/client-go/config/listers/config/v1"
32+ tlsprofile "github.com/openshift/controller-runtime-common/pkg/tls"
3033 "github.com/openshift/library-go/pkg/crypto"
3134
3235 "github.com/openshift/cluster-version-operator/lib/resourcemerge"
@@ -134,6 +137,77 @@ type asyncResult struct {
134137 error error
135138}
136139
140+ type cachedTLSProfile struct {
141+ spec configv1.TLSProfileSpec
142+ apply func (* tls.Config )
143+
144+ // generation is used to detect when the APIServer resource has been updated for caching
145+ generation int64
146+ }
147+
148+ // getAPIServerTLSProfile fetches the cluster TLS profile from APIServer resource.
149+ // On error, returns cached profile or fails if no cache exists.
150+ func getAPIServerTLSProfile (apiServerLister configlistersv1.APIServerLister , lastValidProfile * cachedTLSProfile ) (* cachedTLSProfile , error ) {
151+ apiServer , err := apiServerLister .Get (tlsprofile .APIServerName )
152+ if err != nil {
153+ klog .Errorf ("Failed to get APIServer resource: %v" , err )
154+ return fallbackToCached (lastValidProfile )
155+ }
156+
157+ // Check if the cached profile is still valid based on generation
158+ if lastValidProfile != nil && lastValidProfile .generation == apiServer .Generation {
159+ klog .V (4 ).Info ("Using cached TLS profile (generation unchanged)" )
160+ return lastValidProfile , nil
161+ }
162+
163+ profile , err := tlsprofile .GetTLSProfileSpec (apiServer .Spec .TLSSecurityProfile )
164+ if err != nil {
165+ klog .Errorf ("Failed to resolve TLS profile from APIServer: %v" , err )
166+ return fallbackToCached (lastValidProfile )
167+ }
168+
169+ if lastValidProfile != nil && lastValidProfile .isEqual (& profile ) {
170+ klog .V (4 ).Info ("TLS profile spec unchanged despite generation bump, updating generation" )
171+ return & cachedTLSProfile {
172+ spec : profile ,
173+ apply : lastValidProfile .apply ,
174+ generation : apiServer .Generation ,
175+ }, nil
176+ }
177+
178+ applyTLSProfile , unsupportedCiphers := tlsprofile .NewTLSConfigFromProfile (profile )
179+ if len (unsupportedCiphers ) > 0 {
180+ klog .Warningf ("TLS profile contains unsupported ciphers (will be ignored): %v" , unsupportedCiphers )
181+ }
182+ klog .Infof ("TLS profile changed to: MinTLSVersion=%s, Ciphers=%v" , profile .MinTLSVersion , profile .Ciphers )
183+ return & cachedTLSProfile {
184+ spec : profile ,
185+ apply : applyTLSProfile ,
186+ generation : apiServer .Generation ,
187+ }, nil
188+ }
189+
190+ // fallbackToCached returns the cached profile if available, otherwise returns an error.
191+ func fallbackToCached (lastValidProfile * cachedTLSProfile ) (* cachedTLSProfile , error ) {
192+ if lastValidProfile != nil {
193+ klog .Warningf ("Using last valid TLS profile" )
194+ return lastValidProfile , nil
195+ }
196+ return nil , fmt .Errorf ("no valid TLS profile available" )
197+ }
198+
199+ // isEqual checks if the cached profile matches the given profile spec.
200+ func (c * cachedTLSProfile ) isEqual (profile * configv1.TLSProfileSpec ) bool {
201+ if c == nil && profile == nil {
202+ return true
203+ }
204+ if c == nil || profile == nil {
205+ return false
206+ }
207+ return c .spec .MinTLSVersion == profile .MinTLSVersion &&
208+ slices .Equal (c .spec .Ciphers , profile .Ciphers )
209+ }
210+
137211func createHttpServer (options MetricsOptions , clientCA dynamiccertificates.CAContentProvider ) * http.Server {
138212 if options .DisableAuthentication && options .DisableAuthorization {
139213 handler := http .NewServeMux ()
@@ -276,7 +350,7 @@ type MetricsOptions struct {
276350// Continues serving until runContext.Done() and then attempts a clean
277351// shutdown limited by shutdownContext.Done(). Assumes runContext.Done()
278352// occurs before or simultaneously with shutdownContext.Done().
279- func RunMetrics (runContext context.Context , shutdownContext context.Context , restConfig * rest.Config , options MetricsOptions ) error {
353+ func RunMetrics (runContext context.Context , shutdownContext context.Context , restConfig * rest.Config , apiServerLister configlistersv1. APIServerLister , options MetricsOptions ) error {
280354 if options .ListenAddress == "" {
281355 return errors .New ("listen address is required to serve metrics" )
282356 }
@@ -361,6 +435,7 @@ func RunMetrics(runContext context.Context, shutdownContext context.Context, res
361435 // baseTlSConfig is a template passed to servingCertController,
362436 // which generates updated configs via GetConfigForClient callback on each TLS handshake.
363437 // This enables automatic certificate rotation without server restarts.
438+ // The cluster TLS profile will be applied dynamically in GetConfigForClient.
364439 baseTlSConfig := crypto .SecureTLSConfig (& tls.Config {ClientAuth : clientAuth })
365440 servingCertController := dynamiccertificates .NewDynamicServingCertificateController (
366441 baseTlSConfig ,
@@ -388,6 +463,12 @@ func RunMetrics(runContext context.Context, shutdownContext context.Context, res
388463 }()
389464
390465 server := createHttpServer (options , clientCA )
466+
467+ // lastValidProfile caches the last successfully fetched TLS profile and its apply function.
468+ // On errors, we use this cached value to maintain stability rather than
469+ // constantly switching between profiles on transient errors.
470+ var lastValidProfile * cachedTLSProfile
471+
391472 tlsConfig := crypto .SecureTLSConfig (& tls.Config {
392473 GetConfigForClient : func (clientHello * tls.ClientHelloInfo ) (* tls.Config , error ) {
393474 config , err := servingCertController .GetConfigForClient (clientHello )
@@ -399,6 +480,14 @@ func RunMetrics(runContext context.Context, shutdownContext context.Context, res
399480 err := fmt .Errorf ("serving certificate controller returned nil TLS configuration" )
400481 return nil , err
401482 }
483+
484+ profile , err := getAPIServerTLSProfile (apiServerLister , lastValidProfile )
485+ if err != nil {
486+ return nil , fmt .Errorf ("failed to get TLS profile for metrics server: %w" , err )
487+ }
488+ lastValidProfile = profile
489+ profile .apply (config )
490+
402491 return config , nil
403492 },
404493 })
0 commit comments