Skip to content

Commit befe25e

Browse files
committed
feat(resource builder): allow to inject tls configuration into annotated config maps
Also return an error when APIServer CR is not found to avoid falling back to library-go's ObserveTLSSecurityProfile defaults. These defaults can cause a temporary flip to different TLS configuration that does not adhere the centralized TLS configuration. To avoid any unexpected changes error right away and wait until the CR is available again. Meantime, do not update the corresponding CM.
1 parent 3f9e385 commit befe25e

5 files changed

Lines changed: 1266 additions & 0 deletions

File tree

hack/generate-lib-resources.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ def scheme_group_versions(types):
307307
modifiers = {
308308
('k8s.io/api/apps/v1', 'Deployment'): 'b.modifyDeployment',
309309
('k8s.io/api/apps/v1', 'DaemonSet'): 'b.modifyDaemonSet',
310+
('k8s.io/api/core/v1', 'ConfigMap'): 'b.modifyConfigMap',
310311
}
311312

312313
health_checks = {

lib/resourcebuilder/core.go

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package resourcebuilder
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"sort"
8+
9+
"sigs.k8s.io/kustomize/kyaml/yaml"
10+
11+
corev1 "k8s.io/api/core/v1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
14+
"k8s.io/apimachinery/pkg/labels"
15+
"k8s.io/client-go/tools/cache"
16+
"k8s.io/klog/v2"
17+
"k8s.io/utils/clock"
18+
19+
configv1 "github.com/openshift/api/config/v1"
20+
operatorv1alpha1 "github.com/openshift/api/operator/v1alpha1"
21+
configlistersv1 "github.com/openshift/client-go/config/listers/config/v1"
22+
"github.com/openshift/library-go/pkg/operator/configobserver/apiserver"
23+
"github.com/openshift/library-go/pkg/operator/events"
24+
"github.com/openshift/library-go/pkg/operator/resourcesynccontroller"
25+
)
26+
27+
const (
28+
// ConfigMapInjectTLSAnnotation is the annotation key that triggers TLS injection into ConfigMaps
29+
ConfigMapInjectTLSAnnotation = "config.openshift.io/inject-tls"
30+
)
31+
32+
type optional[T any] struct {
33+
value T
34+
found bool
35+
}
36+
37+
type tlsConfig struct {
38+
minTLSVersion optional[string]
39+
cipherSuites optional[[]string]
40+
}
41+
42+
func (b *builder) modifyConfigMap(ctx context.Context, cm *corev1.ConfigMap) error {
43+
// Check for TLS injection annotation
44+
if value, ok := cm.Annotations[ConfigMapInjectTLSAnnotation]; !ok || value != "true" {
45+
return nil
46+
}
47+
48+
klog.V(2).Infof("ConfigMap %s/%s has %s annotation set to true", cm.Namespace, cm.Name, ConfigMapInjectTLSAnnotation)
49+
50+
// Empty data, nothing to inject into
51+
if cm.Data == nil {
52+
klog.V(2).Infof("ConfigMap %s/%s has empty data, skipping TLS profile injection", cm.Namespace, cm.Name)
53+
return nil
54+
}
55+
56+
// Observe TLS configuration from APIServer
57+
tlsConf, err := b.observeTLSConfiguration(ctx, cm)
58+
if err != nil {
59+
return fmt.Errorf("unable to observe TLS configuration: %v", err)
60+
}
61+
62+
minTLSLog := "<not found>"
63+
if tlsConf.minTLSVersion.found {
64+
minTLSLog = tlsConf.minTLSVersion.value
65+
}
66+
cipherSuitesLog := "<not found>"
67+
if tlsConf.cipherSuites.found {
68+
cipherSuitesLog = fmt.Sprintf("%v", tlsConf.cipherSuites.value)
69+
}
70+
klog.V(4).Infof("ConfigMap %s/%s: observed minTLSVersion=%v, cipherSuites=%v",
71+
cm.Namespace, cm.Name, minTLSLog, cipherSuitesLog)
72+
73+
// Process each data entry that contains GenericOperatorConfig
74+
for key, value := range cm.Data {
75+
klog.V(4).Infof("Processing %q key", key)
76+
// Parse YAML into RNode to preserve formatting and field order
77+
rnode, err := yaml.Parse(value)
78+
if err != nil {
79+
klog.V(4).Infof("ConfigMap's %q entry parsing failed: %v", key, err)
80+
// Not valid YAML, skip this entry
81+
continue
82+
}
83+
84+
// Check if this is a supported config kind
85+
switch {
86+
case rnode.GetKind() == "GenericOperatorConfig" && rnode.GetApiVersion() == operatorv1alpha1.GroupVersion.String():
87+
case rnode.GetKind() == "GenericControllerConfig" && rnode.GetApiVersion() == configv1.GroupVersion.String():
88+
default:
89+
klog.V(4).Infof("ConfigMap's %q entry is not a supported config type. Only GenericOperatorConfig (%v) and GenericControllerConfig (%v) are. Skipping this entry", key, operatorv1alpha1.GroupVersion.String(), configv1.GroupVersion.String())
90+
continue
91+
}
92+
93+
klog.V(2).Infof("ConfigMap %s/%s processing GenericOperatorConfig in key %s", cm.Namespace, cm.Name, key)
94+
95+
// Inject TLS settings into the GenericOperatorConfig while preserving structure
96+
if err := updateRNodeWithTLSSettings(rnode, tlsConf); err != nil {
97+
return fmt.Errorf("failed to inject the TLS configuration: %v", err)
98+
}
99+
100+
// Marshal the modified RNode back to YAML
101+
modifiedYAML, err := rnode.String()
102+
if err != nil {
103+
return fmt.Errorf("failed to marshall the modified ConfigMap back to YAML: %v", err)
104+
}
105+
106+
// Update the ConfigMap data entry with the modified YAML
107+
cm.Data[key] = modifiedYAML
108+
klog.V(2).Infof("ConfigMap %s/%s updated GenericOperatorConfig with TLS profile in key %s", cm.Namespace, cm.Name, key)
109+
}
110+
return nil
111+
}
112+
113+
// observeTLSConfiguration retrieves TLS configuration from the APIServer cluster CR
114+
// using ObserveTLSSecurityProfile and extracts minTLSVersion and cipherSuites.
115+
func (b *builder) observeTLSConfiguration(ctx context.Context, cm *corev1.ConfigMap) (*tlsConfig, error) {
116+
apiServer, err := b.configClientv1.APIServers().Get(ctx, "cluster", metav1.GetOptions{})
117+
if err != nil {
118+
return nil, fmt.Errorf("failed to retrieve APIServer CR for ConfigMap %s/%s: %w", cm.Namespace, cm.Name, err)
119+
}
120+
121+
listers := &configObserverListers{
122+
apiServerLister: &apiServerListerAdapter{
123+
apiServer: apiServer,
124+
},
125+
}
126+
127+
// Create an in-memory event recorder that doesn't send events to the API server
128+
recorder := events.NewInMemoryRecorder("configmap-tls-injection", clock.RealClock{})
129+
130+
// Call ObserveTLSSecurityProfile to get TLS configuration
131+
observedConfig, errs := apiserver.ObserveTLSSecurityProfile(listers, recorder, map[string]any{})
132+
if len(errs) > 0 {
133+
return nil, fmt.Errorf("error observing TLS profile for ConfigMap %s/%s: %w", cm.Namespace, cm.Name, errors.Join(errs...))
134+
}
135+
136+
config := &tlsConfig{}
137+
138+
// Extract minTLSVersion from the observed config
139+
if minTLSVersion, minTLSFound, err := unstructured.NestedString(observedConfig, "servingInfo", "minTLSVersion"); err != nil {
140+
return nil, err
141+
} else if minTLSFound {
142+
config.minTLSVersion = optional[string]{value: minTLSVersion, found: true}
143+
}
144+
145+
// Extract cipherSuites from the observed config
146+
if cipherSuites, ciphersFound, err := unstructured.NestedStringSlice(observedConfig, "servingInfo", "cipherSuites"); err != nil {
147+
return nil, err
148+
} else if ciphersFound {
149+
// Sort cipher suites for consistent ordering
150+
sort.Strings(cipherSuites)
151+
config.cipherSuites = optional[[]string]{value: cipherSuites, found: true}
152+
}
153+
154+
return config, nil
155+
}
156+
157+
// updateRNodeWithTLSSettings injects TLS settings into a GenericOperatorConfig RNode while preserving structure.
158+
// If a field in tlsConf is not found, the corresponding field will be deleted from the RNode.
159+
func updateRNodeWithTLSSettings(rnode *yaml.RNode, tlsConf *tlsConfig) error {
160+
servingInfo, err := rnode.Pipe(yaml.LookupCreate(yaml.MappingNode, "servingInfo"))
161+
if err != nil {
162+
return err
163+
}
164+
165+
// Handle cipherSuites field
166+
if tlsConf.cipherSuites.found {
167+
seqNode := yaml.NewListRNode(tlsConf.cipherSuites.value...)
168+
if err := servingInfo.PipeE(yaml.SetField("cipherSuites", seqNode)); err != nil {
169+
return err
170+
}
171+
} else {
172+
if err := servingInfo.PipeE(yaml.Clear("cipherSuites")); err != nil {
173+
return err
174+
}
175+
}
176+
177+
// Handle minTLSVersion field
178+
if tlsConf.minTLSVersion.found {
179+
if err := servingInfo.PipeE(yaml.SetField("minTLSVersion", yaml.NewStringRNode(tlsConf.minTLSVersion.value))); err != nil {
180+
return err
181+
}
182+
} else {
183+
if err := servingInfo.PipeE(yaml.Clear("minTLSVersion")); err != nil {
184+
return err
185+
}
186+
}
187+
188+
return nil
189+
}
190+
191+
// apiServerListerAdapter adapts an injected APIServer to the lister interface
192+
type apiServerListerAdapter struct {
193+
apiServer *configv1.APIServer
194+
}
195+
196+
func (a *apiServerListerAdapter) List(selector labels.Selector) ([]*configv1.APIServer, error) {
197+
// Not implemented - ObserveTLSSecurityProfile only uses Get()
198+
return nil, nil
199+
}
200+
201+
func (a *apiServerListerAdapter) Get(name string) (*configv1.APIServer, error) {
202+
if name != a.apiServer.Name {
203+
return nil, fmt.Errorf("APIServer %q not found", name)
204+
}
205+
return a.apiServer, nil
206+
}
207+
208+
// configObserverListers implements the configobserver.Listers interface.
209+
// It's expected to be used solely for apiserver.ObserveTLSSecurityProfile.
210+
type configObserverListers struct {
211+
apiServerLister configlistersv1.APIServerLister
212+
}
213+
214+
func (l *configObserverListers) APIServerLister() configlistersv1.APIServerLister {
215+
return l.apiServerLister
216+
}
217+
218+
func (l *configObserverListers) ResourceSyncer() resourcesynccontroller.ResourceSyncer {
219+
// Not needed for TLS observation
220+
return nil
221+
}
222+
223+
func (l *configObserverListers) PreRunHasSynced() []cache.InformerSynced {
224+
// Not needed for TLS observation
225+
return nil
226+
}

0 commit comments

Comments
 (0)