Skip to content

Commit 15eee24

Browse files
committed
feat(resource builder): allow to inject tls configuration into annotated config maps
1 parent 4351f52 commit 15eee24

5 files changed

Lines changed: 989 additions & 5 deletions

File tree

hack/generate-lib-resources.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def generate_resourcebuilder(directory, types, clients, modifiers, health_checks
124124
client_properties['{}Client{}'.format(short_name, version)] = {
125125
'package': package,
126126
'client_short_name': client_short_name,
127-
'type': '*{}.{}'.format(client_short_name, client['type']),
127+
'type': '{}{}.{}'.format("" if 'interface' in client and client['interface'] == True else "*", client_short_name, client['type']),
128128
'protobuf': client['package'].startswith('k8s.io/') and 'kube-aggregator' not in client['package'],
129129
}
130130

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

0 commit comments

Comments
 (0)