Skip to content

Commit bb70c3b

Browse files
Merge pull request #1322 from ingvagabund/configmap-tls-injection
CNTRLPLANE-2777: feat(resource builder): allow to inject tls configuration into annotated config maps
2 parents 42a3805 + 11a9781 commit bb70c3b

529 files changed

Lines changed: 134207 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ require (
3434
k8s.io/klog/v2 v2.130.1
3535
k8s.io/kube-aggregator v0.35.1
3636
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
37+
sigs.k8s.io/kustomize/kyaml v0.21.1
38+
sigs.k8s.io/yaml v1.6.0
3739
)
3840

3941
require (
@@ -44,6 +46,7 @@ require (
4446
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
4547
github.com/fsnotify/fsnotify v1.9.0 // indirect
4648
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
49+
github.com/go-errors/errors v1.4.2 // indirect
4750
github.com/go-openapi/jsonpointer v0.22.1 // indirect
4851
github.com/go-openapi/jsonreference v0.21.2 // indirect
4952
github.com/go-openapi/swag v0.25.1 // indirect
@@ -63,6 +66,7 @@ require (
6366
github.com/google/gnostic-models v0.7.0 // indirect
6467
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
6568
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
69+
github.com/imdario/mergo v0.3.12 // indirect
6670
github.com/inconshreveable/mousetrap v1.1.0 // indirect
6771
github.com/jpillora/backoff v1.0.0 // indirect
6872
github.com/json-iterator/go v1.1.12 // indirect
@@ -98,9 +102,9 @@ require (
98102
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
99103
sigs.k8s.io/controller-runtime v0.22.2 // indirect
100104
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
105+
sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96 // indirect
101106
sigs.k8s.io/randfill v1.0.0 // indirect
102107
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
103-
sigs.k8s.io/yaml v1.6.0 // indirect
104108
)
105109

106110
replace github.com/onsi/ginkgo/v2 => github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20241205171354-8006f302fd12

go.sum

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
2121
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
2222
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
2323
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
24+
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
25+
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
2426
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
2527
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
2628
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
@@ -68,6 +70,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
6870
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
6971
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
7072
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
73+
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
74+
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
7175
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
7276
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
7377
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
@@ -199,6 +203,9 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf
199203
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
200204
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
201205
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
206+
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
207+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
208+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
202209
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
203210
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
204211
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -226,6 +233,10 @@ sigs.k8s.io/controller-runtime v0.22.2 h1:cK2l8BGWsSWkXz09tcS4rJh95iOLney5eawcK5
226233
sigs.k8s.io/controller-runtime v0.22.2/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
227234
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
228235
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
236+
sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96 h1:PFWFSkpArPNJxFX4ZKWAk9NSeRoZaXschn+ULa4xVek=
237+
sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96/go.mod h1:EOBQyBowOUsd7U4CJnMHNE0ri+zCXyouGdLwC/jZU+I=
238+
sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI=
239+
sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
229240
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
230241
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
231242
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=

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

0 commit comments

Comments
 (0)