Skip to content

Commit 7f36c57

Browse files
committed
feat(resource builder): allow to inject tls configuration into annotated config maps
1 parent 96bfdb8 commit 7f36c57

5 files changed

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

0 commit comments

Comments
 (0)