Skip to content

Commit b0ab4f6

Browse files
committed
feat(resource builder): allow to inject tls configuration into annotated config maps
1 parent 3f9e385 commit b0ab4f6

5 files changed

Lines changed: 967 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: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
package resourcebuilder
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"slices"
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+
configclientv1 "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1"
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+
func (b *builder) modifyConfigMap(ctx context.Context, cm *corev1.ConfigMap) error {
33+
// Check for TLS injection annotation
34+
if value, ok := cm.Annotations[ConfigMapInjectTLSAnnotation]; !ok || value != "true" {
35+
return nil
36+
}
37+
38+
klog.V(2).Infof("ConfigMap %s/%s has %s annotation set to true", cm.Namespace, cm.Name, ConfigMapInjectTLSAnnotation)
39+
40+
// Empty data, nothing to inject into
41+
if cm.Data == nil {
42+
klog.V(2).Infof("ConfigMap %s/%s has empty data, skipping", cm.Namespace, cm.Name)
43+
return nil
44+
}
45+
46+
// Observe TLS configuration from APIServer
47+
minTLSVersion, minTLSFound, cipherSuites, ciphersFound, err := b.observeTLSConfiguration(ctx, cm)
48+
if err != nil {
49+
return fmt.Errorf("unable to observe TLS configuration: %v", err)
50+
}
51+
if !minTLSFound && !ciphersFound {
52+
klog.V(2).Infof("ConfigMap %s/%s: no TLS configuration found, skipping", cm.Namespace, cm.Name)
53+
return nil
54+
}
55+
56+
klog.V(4).Infof("Observing minTLSVersion=%v, cipherSuites=%v", minTLSVersion, cipherSuites)
57+
58+
// Process each data entry that contains GenericOperatorConfig
59+
for key, value := range cm.Data {
60+
klog.V(4).Infof("Processing %q key", key)
61+
// Parse YAML into RNode to preserve formatting and field order
62+
rnode, err := yaml.Parse(value)
63+
if err != nil {
64+
klog.V(4).Infof("ConfigMap's %q entry parsing failed: %v", key, err)
65+
// Not valid YAML, skip this entry
66+
continue
67+
}
68+
69+
// Check if this is a GenericOperatorConfig by checking the kind field
70+
kind := rnode.GetKind()
71+
if kind != "GenericOperatorConfig" {
72+
klog.V(4).Infof("ConfigMap's %q entry is not a GenericOperatorConfig, skipping this entry", key)
73+
continue
74+
}
75+
76+
klog.V(2).Infof("ConfigMap %s/%s processing GenericOperatorConfig in key %s", cm.Namespace, cm.Name, key)
77+
78+
// Inject TLS settings into the GenericOperatorConfig while preserving structure
79+
if err := updateRNodeWithTLSSettings(rnode, minTLSVersion, minTLSFound, cipherSuites, ciphersFound); err != nil {
80+
klog.V(4).Infof("Error injecting the TLS configuration: %v", err)
81+
return err
82+
}
83+
84+
// Marshal the modified RNode back to YAML
85+
modifiedYAML, err := rnode.String()
86+
if err != nil {
87+
klog.V(4).Infof("Error marshalling the modified ConfigMap back to YAML: %v", err)
88+
return err
89+
}
90+
91+
// Update the ConfigMap data entry with the modified YAML
92+
cm.Data[key] = modifiedYAML
93+
klog.V(2).Infof("ConfigMap %s/%s updated GenericOperatorConfig in key %s with %d ciphers and minTLSVersion=%s",
94+
cm.Namespace, cm.Name, key, len(cipherSuites), minTLSVersion)
95+
}
96+
97+
klog.V(2).Infof("APIServer config available for ConfigMap %s/%s TLS injection", cm.Namespace, cm.Name)
98+
99+
return nil
100+
}
101+
102+
// observeTLSConfiguration retrieves TLS configuration from the APIServer cluster CR
103+
// using ObserveTLSSecurityProfile and extracts minTLSVersion and cipherSuites.
104+
// minTLSVersion string, minTLSFound bool, cipherSuites []string, ciphersFound bool, err error
105+
func (b *builder) observeTLSConfiguration(ctx context.Context, cm *corev1.ConfigMap) (string, bool, []string, bool, error) {
106+
// Create a lister adapter for ObserveTLSSecurityProfile
107+
lister := &apiServerListerAdapter{
108+
client: b.configClientv1.APIServers(),
109+
ctx: ctx,
110+
}
111+
listers := &configObserverListers{
112+
apiServerLister: lister,
113+
}
114+
115+
// Create an in-memory event recorder that doesn't send events to the API server
116+
recorder := events.NewInMemoryRecorder("configmap-tls-injection", clock.RealClock{})
117+
118+
// Call ObserveTLSSecurityProfile to get TLS configuration
119+
observedConfig, errs := apiserver.ObserveTLSSecurityProfile(listers, recorder, map[string]any{})
120+
if len(errs) > 0 {
121+
// Log errors but continue - ObserveTLSSecurityProfile is tolerant of missing config
122+
for _, err := range errs {
123+
klog.Errorf("ConfigMap %s/%s: error observing TLS profile: %v", cm.Namespace, cm.Name, err)
124+
}
125+
}
126+
127+
// Extract the TLS settings from the observed config
128+
minTLSVersion, minTLSFound, err := unstructured.NestedString(observedConfig, "servingInfo", "minTLSVersion")
129+
if err != nil {
130+
// This error is unlikely to happen unless unstructured.NestedString is buggy.
131+
// From unstructured.NestedString's description:
132+
// "Returns false if value is not found and an error if not a string."
133+
// The observedConfig's servingInfo.minTLSVersion is of a string type
134+
return "", false, nil, false, err
135+
}
136+
cipherSuites, ciphersFound, _ := unstructured.NestedStringSlice(observedConfig, "servingInfo", "cipherSuites")
137+
if err != nil {
138+
// This error is unlikely to happen unless unstructured.NestedStringSlice is buggy
139+
// From unstructured.NestedString's description:
140+
// "Returns false if value is not found and an error if not a []interface{} or contains non-string items in the slice."
141+
// The observedConfig's servingInfo.minTLSVersion is of a string type
142+
return "", false, nil, false, err
143+
}
144+
145+
// Sort cipher suites for consistent comparison
146+
if ciphersFound && len(cipherSuites) > 0 {
147+
sort.Strings(cipherSuites)
148+
}
149+
150+
return minTLSVersion, minTLSFound, cipherSuites, ciphersFound, nil
151+
}
152+
153+
// updateRNodeWithTLSSettings injects TLS settings into a GenericOperatorConfig RNode while preserving structure
154+
// cipherSuites is expected to be sorted
155+
func updateRNodeWithTLSSettings(rnode *yaml.RNode, minTLSVersion string, minTLSFound bool, cipherSuites []string, ciphersFound bool) error {
156+
servingInfo, err := rnode.Pipe(yaml.LookupCreate(yaml.MappingNode, "servingInfo"))
157+
if err != nil {
158+
return err
159+
}
160+
161+
if ciphersFound {
162+
currentCiphers, err := getSortedCipherSuites(servingInfo)
163+
if err != nil {
164+
return err
165+
}
166+
if !slices.Equal(currentCiphers, cipherSuites) {
167+
// Create a sequence node with the cipher suites
168+
seqNode := yaml.NewListRNode(cipherSuites...)
169+
if err := servingInfo.PipeE(yaml.SetField("cipherSuites", seqNode)); err != nil {
170+
return err
171+
}
172+
}
173+
}
174+
175+
// Update minTLSVersion if found
176+
if minTLSFound {
177+
if err := servingInfo.PipeE(yaml.SetField("minTLSVersion", yaml.NewStringRNode(minTLSVersion))); err != nil {
178+
return err
179+
}
180+
}
181+
182+
return nil
183+
}
184+
185+
// getSortedCipherSuites extracts and sorts the cipherSuites string slice from a servingInfo RNode
186+
func getSortedCipherSuites(servingInfo *yaml.RNode) ([]string, error) {
187+
ciphersNode, err := servingInfo.Pipe(yaml.Lookup("cipherSuites"))
188+
if err != nil || ciphersNode == nil {
189+
return nil, err
190+
}
191+
192+
elements, err := ciphersNode.Elements()
193+
if err != nil {
194+
return nil, err
195+
}
196+
197+
var ciphers []string
198+
for _, elem := range elements {
199+
// For scalar nodes, access the value directly without YAML serialization
200+
// This avoids the trailing newline that String() (which uses yaml.Encode) adds
201+
if elem.YNode().Kind == yaml.ScalarNode {
202+
value := elem.YNode().Value
203+
// Skip empty values
204+
if value == "" {
205+
continue
206+
}
207+
ciphers = append(ciphers, value)
208+
}
209+
}
210+
211+
// Sort cipher suites for consistent comparison
212+
sort.Strings(ciphers)
213+
214+
return ciphers, nil
215+
}
216+
217+
// apiServerListerAdapter adapts a client interface to the lister interface
218+
type apiServerListerAdapter struct {
219+
client configclientv1.APIServerInterface
220+
ctx context.Context
221+
}
222+
223+
func (a *apiServerListerAdapter) List(selector labels.Selector) ([]*configv1.APIServer, error) {
224+
// Not implemented - ObserveTLSSecurityProfile only uses Get()
225+
return nil, nil
226+
}
227+
228+
func (a *apiServerListerAdapter) Get(name string) (*configv1.APIServer, error) {
229+
return a.client.Get(a.ctx, name, metav1.GetOptions{})
230+
}
231+
232+
// configObserverListers implements the configobserver.Listers interface.
233+
// It's expected to be used solely for apiserver.ObserveTLSSecurityProfile.
234+
type configObserverListers struct {
235+
apiServerLister configlistersv1.APIServerLister
236+
}
237+
238+
func (l *configObserverListers) APIServerLister() configlistersv1.APIServerLister {
239+
return l.apiServerLister
240+
}
241+
242+
func (l *configObserverListers) ResourceSyncer() resourcesynccontroller.ResourceSyncer {
243+
// Not needed for TLS observation
244+
return nil
245+
}
246+
247+
func (l *configObserverListers) PreRunHasSynced() []cache.InformerSynced {
248+
// Not needed for TLS observation
249+
return nil
250+
}

0 commit comments

Comments
 (0)