Skip to content

Commit 18158e8

Browse files
committed
wip
1 parent 5dfcd8b commit 18158e8

3 files changed

Lines changed: 108 additions & 2 deletions

File tree

pkg/cvo/cvo.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ type Operator struct {
122122
cmConfigManagedLister listerscorev1.ConfigMapNamespaceLister
123123
proxyLister configlistersv1.ProxyLister
124124
featureGateLister configlistersv1.FeatureGateLister
125+
apiServerLister configlistersv1.APIServerLister
125126
cacheSynced []cache.InformerSynced
126127

127128
// queue tracks applying updates to a cluster.
@@ -223,6 +224,7 @@ func New(
223224
proxyInformer configinformersv1.ProxyInformer,
224225
operatorInformerFactory operatorexternalversions.SharedInformerFactory,
225226
featureGateInformer configinformersv1.FeatureGateInformer,
227+
apiServerInformer configinformersv1.APIServerInformer,
226228
client clientset.Interface,
227229
kubeClient kubernetes.Interface,
228230
operatorClient operatorclientset.Interface,
@@ -305,6 +307,8 @@ func New(
305307
optr.featureGateLister = featureGateInformer.Lister()
306308
optr.cacheSynced = append(optr.cacheSynced, featureGateInformer.Informer().HasSynced)
307309

310+
optr.apiServerLister = apiServerInformer.Lister()
311+
308312
// make sure this is initialized after all the listers are initialized
309313
optr.upgradeableChecks = optr.defaultUpgradeableChecks()
310314

@@ -1184,3 +1188,8 @@ func (optr *Operator) shouldReconcileAcceptRisks() bool {
11841188
// HyperShift will be supported later if needed
11851189
return optr.enabledCVOFeatureGates.AcceptRisks() && !optr.hypershift
11861190
}
1191+
1192+
// APIServerLister returns the APIServer lister for accessing cluster TLS configuration
1193+
func (optr *Operator) APIServerLister() configlistersv1.APIServerLister {
1194+
return optr.apiServerLister
1195+
}

pkg/cvo/metrics.go

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"net"
1010
"net/http"
11+
"slices"
1112
"time"
1213

1314
"github.com/prometheus/client_golang/prometheus"
@@ -26,6 +27,8 @@ import (
2627
"k8s.io/klog/v2"
2728

2829
configv1 "github.com/openshift/api/config/v1"
30+
configlistersv1 "github.com/openshift/client-go/config/listers/config/v1"
31+
tlsprofile "github.com/openshift/controller-runtime-common/pkg/tls"
2932
"github.com/openshift/library-go/pkg/crypto"
3033

3134
"github.com/openshift/cluster-version-operator/lib/resourcemerge"
@@ -128,6 +131,81 @@ type asyncResult struct {
128131
error error
129132
}
130133

134+
// cachedTLSProfile holds the profile spec, apply function, and observed generation.
135+
// The apply function is computed once when the profile changes to avoid
136+
// calling NewTLSConfigFromProfile on every TLS handshake.
137+
// The generation is used to detect when the APIServer resource has been updated.
138+
type cachedTLSProfile struct {
139+
spec configv1.TLSProfileSpec
140+
apply func(*tls.Config)
141+
generation int64
142+
}
143+
144+
// getAPIServerTLSProfile fetches the cluster TLS profile from APIServer resource
145+
// and returns the updated cache. This is called on each TLS handshake.
146+
// On error, returns the last successfully fetched profile if available.
147+
// Returns an error if no valid profile has ever been fetched (fails closed for security).
148+
func getAPIServerTLSProfile(apiServerLister configlistersv1.APIServerLister, lastValidProfile *cachedTLSProfile) (*cachedTLSProfile, error) {
149+
apiServer, err := apiServerLister.Get(tlsprofile.APIServerName)
150+
if err != nil {
151+
klog.Errorf("Failed to get APIServer resource: %v", err)
152+
return fallbackToCached(lastValidProfile)
153+
}
154+
155+
// Check if the cached profile is still valid based on generation
156+
if lastValidProfile != nil && lastValidProfile.generation == apiServer.Generation {
157+
klog.V(4).Info("Using cached TLS profile (generation unchanged)")
158+
return lastValidProfile, nil
159+
}
160+
161+
profile, err := tlsprofile.GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile)
162+
if err != nil {
163+
klog.Errorf("Failed to resolve TLS profile from APIServer: %v", err)
164+
return fallbackToCached(lastValidProfile)
165+
}
166+
167+
if lastValidProfile != nil && lastValidProfile.isEqual(&profile) {
168+
klog.V(4).Info("TLS profile spec unchanged despite generation bump, updating generation")
169+
return &cachedTLSProfile{
170+
spec: profile,
171+
apply: lastValidProfile.apply,
172+
generation: apiServer.Generation,
173+
}, nil
174+
}
175+
176+
applyTLSProfile, unsupportedCiphers := tlsprofile.NewTLSConfigFromProfile(profile)
177+
if len(unsupportedCiphers) > 0 {
178+
klog.Warningf("TLS profile contains unsupported ciphers (will be ignored): %v", unsupportedCiphers)
179+
}
180+
klog.Infof("TLS profile changed to: MinTLSVersion=%s, Ciphers=%s", profile.MinTLSVersion, profile.Ciphers)
181+
return &cachedTLSProfile{
182+
spec: profile,
183+
apply: applyTLSProfile,
184+
generation: apiServer.Generation,
185+
}, nil
186+
}
187+
188+
// fallbackToCached returns the cached profile if available, otherwise returns an error.
189+
func fallbackToCached(lastValidProfile *cachedTLSProfile) (*cachedTLSProfile, error) {
190+
if lastValidProfile != nil {
191+
klog.Warningf("Using last valid TLS profile")
192+
return lastValidProfile, nil
193+
}
194+
return nil, fmt.Errorf("no valid TLS profile available")
195+
}
196+
197+
// isEqual checks if the cached profile matches the given profile spec.
198+
func (c *cachedTLSProfile) isEqual(profile *configv1.TLSProfileSpec) bool {
199+
if c == nil && profile == nil {
200+
return true
201+
}
202+
if c == nil || profile == nil {
203+
return false
204+
}
205+
return c.spec.MinTLSVersion == profile.MinTLSVersion &&
206+
slices.Equal(c.spec.Ciphers, profile.Ciphers)
207+
}
208+
131209
func createHttpServer(options MetricsOptions, clientCA dynamiccertificates.CAContentProvider) *http.Server {
132210
if options.DisableAuthentication && options.DisableAuthorization {
133211
handler := http.NewServeMux()
@@ -270,7 +348,7 @@ type MetricsOptions struct {
270348
// Continues serving until runContext.Done() and then attempts a clean
271349
// shutdown limited by shutdownContext.Done(). Assumes runContext.Done()
272350
// occurs before or simultaneously with shutdownContext.Done().
273-
func RunMetrics(runContext context.Context, shutdownContext context.Context, restConfig *rest.Config, options MetricsOptions) error {
351+
func RunMetrics(runContext context.Context, shutdownContext context.Context, restConfig *rest.Config, apiServerLister configlistersv1.APIServerLister, options MetricsOptions) error {
274352
if options.ListenAddress == "" {
275353
return errors.New("listen address is required to serve metrics")
276354
}
@@ -355,6 +433,7 @@ func RunMetrics(runContext context.Context, shutdownContext context.Context, res
355433
// baseTlSConfig is a template passed to servingCertController,
356434
// which generates updated configs via GetConfigForClient callback on each TLS handshake.
357435
// This enables automatic certificate rotation without server restarts.
436+
// The cluster TLS profile will be applied dynamically in GetConfigForClient.
358437
baseTlSConfig := crypto.SecureTLSConfig(&tls.Config{ClientAuth: clientAuth})
359438
servingCertController := dynamiccertificates.NewDynamicServingCertificateController(
360439
baseTlSConfig,
@@ -382,6 +461,12 @@ func RunMetrics(runContext context.Context, shutdownContext context.Context, res
382461
}()
383462

384463
server := createHttpServer(options, clientCA)
464+
465+
// lastValidProfile caches the last successfully fetched TLS profile and its apply function.
466+
// On errors, we use this cached value to maintain stability rather than
467+
// constantly switching between profiles on transient errors.
468+
var lastValidProfile *cachedTLSProfile
469+
385470
tlsConfig := crypto.SecureTLSConfig(&tls.Config{
386471
GetConfigForClient: func(clientHello *tls.ClientHelloInfo) (*tls.Config, error) {
387472
config, err := servingCertController.GetConfigForClient(clientHello)
@@ -393,6 +478,17 @@ func RunMetrics(runContext context.Context, shutdownContext context.Context, res
393478
err := fmt.Errorf("serving certificate controller returned nil TLS configuration")
394479
return nil, err
395480
}
481+
482+
// Fetch cluster TLS profile from APIServer resource (cached via lister, O(1) lookup)
483+
// and apply it to the config. This allows dynamic updates without CVO restart.
484+
// Fail closed if no valid profile is available (security).
485+
profile, err := getAPIServerTLSProfile(apiServerLister, lastValidProfile)
486+
if err != nil {
487+
return nil, fmt.Errorf("failed to get TLS profile for metrics server: %w", err)
488+
}
489+
lastValidProfile = profile
490+
profile.apply(config)
491+
396492
return config, nil
397493
},
398494
})

pkg/start/start.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ func (o *Options) run(ctx context.Context, controllerCtx *Context, lock resource
356356
resultChannelCount++
357357
go func() {
358358
defer utilruntime.HandleCrash()
359-
err := cvo.RunMetrics(postMainContext, shutdownContext, restConfig, o.MetricsOptions)
359+
err := cvo.RunMetrics(postMainContext, shutdownContext, restConfig, controllerCtx.CVO.APIServerLister(), o.MetricsOptions)
360360
resultChannel <- asyncResult{name: "metrics server", error: err}
361361
}()
362362
}
@@ -615,6 +615,7 @@ func (o *Options) NewControllerContext(
615615
configInformerFactory.Config().V1().Proxies(),
616616
operatorInformerFactory,
617617
configInformerFactory.Config().V1().FeatureGates(),
618+
configInformerFactory.Config().V1().APIServers(),
618619
cb.ClientOrDie(o.Namespace),
619620
cvoKubeClient,
620621
operatorClient,

0 commit comments

Comments
 (0)