Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ require (
k8s.io/klog/v2 v2.130.1
k8s.io/kube-aggregator v0.34.1
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
sigs.k8s.io/kustomize/kyaml v0.13.6
sigs.k8s.io/yaml v1.6.0
)

require (
Expand All @@ -43,6 +45,7 @@ require (
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/swag v0.25.1 // indirect
Expand All @@ -63,6 +66,7 @@ require (
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand Down Expand Up @@ -101,9 +105,9 @@ require (
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
sigs.k8s.io/controller-runtime v0.22.2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

replace github.com/onsi/ginkgo/v2 => github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20241205171354-8006f302fd12
572 changes: 572 additions & 0 deletions go.sum

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions hack/generate-lib-resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ def scheme_group_versions(types):
modifiers = {
('k8s.io/api/apps/v1', 'Deployment'): 'b.modifyDeployment',
('k8s.io/api/apps/v1', 'DaemonSet'): 'b.modifyDaemonSet',
('k8s.io/api/core/v1', 'ConfigMap'): 'b.modifyConfigMap',
}

health_checks = {
Expand Down
220 changes: 220 additions & 0 deletions lib/resourcebuilder/core.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package resourcebuilder

import (
"context"
"errors"
"fmt"
"sort"

"sigs.k8s.io/kustomize/kyaml/yaml"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
"k8s.io/utils/clock"

configv1 "github.com/openshift/api/config/v1"
operatorv1alpha1 "github.com/openshift/api/operator/v1alpha1"
configclientv1 "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1"
configlistersv1 "github.com/openshift/client-go/config/listers/config/v1"
"github.com/openshift/library-go/pkg/operator/configobserver/apiserver"
"github.com/openshift/library-go/pkg/operator/events"
"github.com/openshift/library-go/pkg/operator/resourcesynccontroller"
)

const (
// ConfigMapInjectTLSAnnotation is the annotation key that triggers TLS injection into ConfigMaps
ConfigMapInjectTLSAnnotation = "config.openshift.io/inject-tls"
)

type optional[T any] struct {
value T
found bool
}

type tlsConfig struct {
minTLSVersion optional[string]
cipherSuites optional[[]string]
}

func (b *builder) modifyConfigMap(ctx context.Context, cm *corev1.ConfigMap) error {
// Check for TLS injection annotation
if value, ok := cm.Annotations[ConfigMapInjectTLSAnnotation]; !ok || value != "true" {
return nil
}

klog.V(2).Infof("ConfigMap %s/%s has %s annotation set to true", cm.Namespace, cm.Name, ConfigMapInjectTLSAnnotation)

// Empty data, nothing to inject into
if cm.Data == nil {
klog.V(2).Infof("ConfigMap %s/%s has empty data, skipping TLS profile injection", cm.Namespace, cm.Name)
return nil
}

// Observe TLS configuration from APIServer
tlsConf, err := b.observeTLSConfiguration(ctx, cm)
if err != nil {
return fmt.Errorf("unable to observe TLS configuration: %v", err)
}

minTLSLog := "<not found>"
if tlsConf.minTLSVersion.found {
minTLSLog = tlsConf.minTLSVersion.value
}
cipherSuitesLog := "<not found>"
if tlsConf.cipherSuites.found {
cipherSuitesLog = fmt.Sprintf("%v", tlsConf.cipherSuites.value)
}
klog.V(4).Infof("ConfigMap %s/%s: observed minTLSVersion=%v, cipherSuites=%v",
cm.Namespace, cm.Name, minTLSLog, cipherSuitesLog)

// Process each data entry that contains GenericOperatorConfig
for key, value := range cm.Data {
klog.V(4).Infof("Processing %q key", key)
// Parse YAML into RNode to preserve formatting and field order
rnode, err := yaml.Parse(value)
if err != nil {
klog.V(4).Infof("ConfigMap's %q entry parsing failed: %v", key, err)
// Not valid YAML, skip this entry
continue
}

// Check if this is a supported GenericOperatorConfig kind
if rnode.GetKind() != "GenericOperatorConfig" || rnode.GetApiVersion() != operatorv1alpha1.GroupVersion.String() {
Copy link

@ricardomaraschini ricardomaraschini Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that GenericOperatorConfig is something that has been left alone in v1alpha1 and has been replaced by GenericControllerConfig on configv1. We need to support both, specially now we want to keep things consistent with the work we are doing on hypershift.

cc @ingvagabund

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apiVersion check

klog.V(4).Infof("ConfigMap's %q entry is not a supported GenericOperatorConfig, skipping this entry", key)
continue
}

klog.V(2).Infof("ConfigMap %s/%s processing GenericOperatorConfig in key %s", cm.Namespace, cm.Name, key)

// Inject TLS settings into the GenericOperatorConfig while preserving structure
if err := updateRNodeWithTLSSettings(rnode, tlsConf); err != nil {
return fmt.Errorf("failed to inject the TLS configuration: %v", err)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bubbling up some context of errors

}

// Marshal the modified RNode back to YAML
modifiedYAML, err := rnode.String()
if err != nil {
return fmt.Errorf("failed to marshall the modified ConfigMap back to YAML: %v", err)
}

// Update the ConfigMap data entry with the modified YAML
cm.Data[key] = modifiedYAML
klog.V(2).Infof("ConfigMap %s/%s updated GenericOperatorConfig with TLS profile in key %s", cm.Namespace, cm.Name, key)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the tls configuration remains the same across all keys, no need to log it on every processed key

}
return nil
}

// observeTLSConfiguration retrieves TLS configuration from the APIServer cluster CR
// using ObserveTLSSecurityProfile and extracts minTLSVersion and cipherSuites.
func (b *builder) observeTLSConfiguration(ctx context.Context, cm *corev1.ConfigMap) (*tlsConfig, error) {
// Create a lister adapter for ObserveTLSSecurityProfile
lister := &apiServerListerAdapter{
client: b.configClientv1.APIServers(),
ctx: ctx,
}
listers := &configObserverListers{
apiServerLister: lister,
}

// Create an in-memory event recorder that doesn't send events to the API server
recorder := events.NewInMemoryRecorder("configmap-tls-injection", clock.RealClock{})

// Call ObserveTLSSecurityProfile to get TLS configuration
observedConfig, errs := apiserver.ObserveTLSSecurityProfile(listers, recorder, map[string]any{})
if len(errs) > 0 {
return nil, fmt.Errorf("error observing TLS profile for ConfigMap %s/%s: %w", cm.Namespace, cm.Name, errors.Join(errs...))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hard failure; here i would like us to be defensive with testing to make sure any future bumps won't cause issues

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

however, we already have extensive testing, I believe

}

config := &tlsConfig{}

// Extract minTLSVersion from the observed config
if minTLSVersion, minTLSFound, err := unstructured.NestedString(observedConfig, "servingInfo", "minTLSVersion"); err != nil {
return nil, err
} else if minTLSFound {
config.minTLSVersion = optional[string]{value: minTLSVersion, found: true}
}

// Extract cipherSuites from the observed config
if cipherSuites, ciphersFound, err := unstructured.NestedStringSlice(observedConfig, "servingInfo", "cipherSuites"); err != nil {
return nil, err
} else if ciphersFound {
// Sort cipher suites for consistent ordering
sort.Strings(cipherSuites)
config.cipherSuites = optional[[]string]{value: cipherSuites, found: true}
}

return config, nil
}

// updateRNodeWithTLSSettings injects TLS settings into a GenericOperatorConfig RNode while preserving structure.
// If a field in tlsConf is not found, the corresponding field will be deleted from the RNode.
func updateRNodeWithTLSSettings(rnode *yaml.RNode, tlsConf *tlsConfig) error {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New unit tests to ensure the YAML processing remains functional.

servingInfo, err := rnode.Pipe(yaml.LookupCreate(yaml.MappingNode, "servingInfo"))
if err != nil {
return err
}

// Handle cipherSuites field
if tlsConf.cipherSuites.found {
seqNode := yaml.NewListRNode(tlsConf.cipherSuites.value...)
if err := servingInfo.PipeE(yaml.SetField("cipherSuites", seqNode)); err != nil {
return err
}
} else {
if err := servingInfo.PipeE(yaml.Clear("cipherSuites")); err != nil {
return err
}
}

// Handle minTLSVersion field
if tlsConf.minTLSVersion.found {
if err := servingInfo.PipeE(yaml.SetField("minTLSVersion", yaml.NewStringRNode(tlsConf.minTLSVersion.value))); err != nil {
return err
}
} else {
if err := servingInfo.PipeE(yaml.Clear("minTLSVersion")); err != nil {
Copy link
Contributor Author

@DavidHurta DavidHurta Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clearing if not found to honor the observeTLSConfiguration function

return err
}
}

return nil
}

// apiServerListerAdapter adapts a client interface to the lister interface
type apiServerListerAdapter struct {
client configclientv1.APIServerInterface
ctx context.Context
}

func (a *apiServerListerAdapter) List(selector labels.Selector) ([]*configv1.APIServer, error) {
// Not implemented - ObserveTLSSecurityProfile only uses Get()
return nil, nil
}

func (a *apiServerListerAdapter) Get(name string) (*configv1.APIServer, error) {
return a.client.Get(a.ctx, name, metav1.GetOptions{})
}

// configObserverListers implements the configobserver.Listers interface.
// It's expected to be used solely for apiserver.ObserveTLSSecurityProfile.
type configObserverListers struct {
apiServerLister configlistersv1.APIServerLister
}

func (l *configObserverListers) APIServerLister() configlistersv1.APIServerLister {
return l.apiServerLister
}

func (l *configObserverListers) ResourceSyncer() resourcesynccontroller.ResourceSyncer {
// Not needed for TLS observation
return nil
}

func (l *configObserverListers) PreRunHasSynced() []cache.InformerSynced {
// Not needed for TLS observation
return nil
}
Loading