Skip to content
This repository was archived by the owner on Aug 12, 2024. It is now read-only.

Commit fd8a0d9

Browse files
committed
Add ConfigMapSyncer controller
The ConfigMapSyncer syncs secret data to configmaps based on injection annotations present in configmaps in watched namespaces. We include a rukpak-ca configmap with these annotations present so that cluster administrators can share rukpak-ca trust without exposing the CA key that's present in the rukpak-ca secret. Signed-off-by: Joe Lanford <joe.lanford@gmail.com>
1 parent 214a389 commit fd8a0d9

File tree

5 files changed

+239
-5
lines changed

5 files changed

+239
-5
lines changed

cmd/core/main.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ import (
3636
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
3737
ctrl "sigs.k8s.io/controller-runtime"
3838
"sigs.k8s.io/controller-runtime/pkg/cache"
39+
"sigs.k8s.io/controller-runtime/pkg/cluster"
3940
crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer"
4041
"sigs.k8s.io/controller-runtime/pkg/healthz"
4142
"sigs.k8s.io/controller-runtime/pkg/log/zap"
4243

4344
rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1"
45+
"github.com/operator-framework/rukpak/internal/configmapsyncer"
4446
"github.com/operator-framework/rukpak/internal/finalizer"
4547
plaincontrollers "github.com/operator-framework/rukpak/internal/provisioner/plain/controllers"
4648
registrycontrollers "github.com/operator-framework/rukpak/internal/provisioner/registry/controllers"
@@ -137,6 +139,26 @@ func main() {
137139
}
138140

139141
ns := util.PodNamespace(systemNamespace)
142+
143+
// TODO: use cache.MultiNamespacedCacheWithOptionsBuilder for core controller cache
144+
// When https://github.com/kubernetes-sigs/controller-runtime/pull/1962
145+
// merges, use cache.MultiNamespacedCacheWithOptionsBuilder so that
146+
// the system namespace can use cache.New and watch all objects in its own
147+
// namespace. Once we switch to that cache, we can avoid using a separate
148+
// controller-runtime "cluster".
149+
systemNamespaceClstr, err := cluster.New(cfg, func(options *cluster.Options) {
150+
options.Namespace = ns
151+
})
152+
if err != nil {
153+
setupLog.Error(err, "unable to create manager")
154+
os.Exit(1)
155+
}
156+
157+
if err := mgr.Add(systemNamespaceClstr); err != nil {
158+
setupLog.Error(err, "unable to add system namespace cluster to manager")
159+
os.Exit(1)
160+
}
161+
140162
storageURL, err := url.Parse(fmt.Sprintf("%s/bundles/", httpExternalAddr))
141163
if err != nil {
142164
setupLog.Error(err, "unable to parse bundle content server URL")
@@ -238,6 +260,14 @@ func main() {
238260
setupLog.Error(err, "unable to create controller", "controller", rukpakv1alpha1.BundleDeploymentKind)
239261
os.Exit(1)
240262
}
263+
264+
if err = (&configmapsyncer.Reconciler{
265+
Client: systemNamespaceClstr.GetClient(),
266+
Cache: systemNamespaceClstr.GetCache(),
267+
}).SetupWithManager(mgr); err != nil {
268+
setupLog.Error(err, "unable to create controller", "controller", "ConfigMap")
269+
os.Exit(1)
270+
}
241271
//+kubebuilder:scaffold:builder
242272

243273
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package configmapsyncer
2+
3+
import (
4+
"context"
5+
6+
corev1 "k8s.io/api/core/v1"
7+
"k8s.io/apimachinery/pkg/api/equality"
8+
"k8s.io/apimachinery/pkg/types"
9+
ctrl "sigs.k8s.io/controller-runtime"
10+
"sigs.k8s.io/controller-runtime/pkg/cache"
11+
"sigs.k8s.io/controller-runtime/pkg/client"
12+
"sigs.k8s.io/controller-runtime/pkg/controller"
13+
"sigs.k8s.io/controller-runtime/pkg/handler"
14+
"sigs.k8s.io/controller-runtime/pkg/log"
15+
"sigs.k8s.io/controller-runtime/pkg/predicate"
16+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
17+
"sigs.k8s.io/controller-runtime/pkg/source"
18+
)
19+
20+
const (
21+
configMapInjectFromSecretName = "core.rukpak.io/inject-from-secret-name"
22+
configMapInjectFromSecretKey = "core.rukpak.io/inject-from-secret-key"
23+
configMapInjectToDataKey = "core.rukpak.io/inject-to-data-key"
24+
configMapInjectToBinaryDataKey = "core.rukpak.io/inject-to-binarydata-key"
25+
)
26+
27+
// Reconciler syncs secret fields to configmaps.
28+
type Reconciler struct {
29+
Client client.Client
30+
Cache cache.Cache
31+
}
32+
33+
// Reconcile syncs a secret field to a configmap field based on the presence
34+
// of annotations "core.rukpak.io/inject-from-secret-name" and
35+
// "core.rukpak.io/inject-from-secret-key" (configmaps without BOTH of these
36+
// annotations are ignored). When these annotations are present, this
37+
// reconciler manages all content in the data and binaryData fields.
38+
//
39+
// If the annotation "core.rukpak.io/inject-to-data-key" is present, Reconcile
40+
// creates a data key containing the secret value. Otherwise, the configmap
41+
// data will be empty.
42+
//
43+
// If the annotation "core.rukpak.io/inject-to-binarydata-key" is present,
44+
// Reconcile creates a binaryData key containing the secret value. Otherwise,
45+
// the configmap binaryData will be empty.
46+
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
47+
l := log.FromContext(ctx)
48+
l.V(1).Info("starting reconciliation")
49+
defer l.V(1).Info("ending reconciliation")
50+
51+
// Get configmap from cache and lookup its secret name annotation
52+
cm := &corev1.ConfigMap{}
53+
if err := r.Client.Get(ctx, req.NamespacedName, cm); err != nil {
54+
return ctrl.Result{}, client.IgnoreNotFound(err)
55+
}
56+
57+
secretName, ok := cm.Annotations[configMapInjectFromSecretName]
58+
if !ok {
59+
return ctrl.Result{}, nil
60+
}
61+
62+
// Get referenced secret name (in the same namespace as the configmap)
63+
secret := &corev1.Secret{}
64+
l.V(1).Info("inject from secret", "secretName", secretName)
65+
if err := r.Client.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: secretName}, secret); err != nil {
66+
return ctrl.Result{}, client.IgnoreNotFound(err)
67+
}
68+
69+
// Get the referenced secret's key and value
70+
secretKey, ok := cm.Annotations[configMapInjectFromSecretKey]
71+
if !ok {
72+
return ctrl.Result{}, nil
73+
}
74+
secretValue := secret.Data[secretKey]
75+
76+
// If the configmap asks for injection into a binaryData key
77+
// generate the expected binary data.
78+
cmBinaryDataKey := cm.Annotations[configMapInjectToBinaryDataKey]
79+
expectedBinaryData := map[string][]byte(nil)
80+
if cmBinaryDataKey != "" {
81+
expectedBinaryData = map[string][]byte{cmBinaryDataKey: secretValue}
82+
}
83+
84+
// If the configmap asks for injection into a data key
85+
// generate the expected data.
86+
cmDataKey := cm.Annotations[configMapInjectToDataKey]
87+
expectedData := map[string]string(nil)
88+
if cmDataKey != "" {
89+
expectedData = map[string]string{cmDataKey: string(secretValue)}
90+
}
91+
92+
// If binaryData and data already have the expected contents,
93+
// there's no need to do anything, so return early.
94+
if equality.Semantic.DeepEqual(cm.BinaryData, expectedBinaryData) && equality.Semantic.DeepEqual(cm.Data, expectedData) {
95+
return ctrl.Result{}, nil
96+
}
97+
98+
// Set the expected binaryData and data fields on the configmap
99+
// and then update it.
100+
cm.BinaryData = expectedBinaryData
101+
cm.Data = expectedData
102+
return ctrl.Result{}, r.Client.Update(ctx, cm)
103+
}
104+
105+
// SetupWithManager sets up the controller with the Manager.
106+
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
107+
ctrlr, err := controller.New("configmapsyncer", mgr, controller.Options{
108+
Reconciler: r,
109+
})
110+
if err != nil {
111+
return err
112+
}
113+
114+
if err := ctrlr.Watch(
115+
source.NewKindWithCache(&corev1.ConfigMap{}, r.Cache),
116+
&handler.EnqueueRequestForObject{},
117+
configMapHasInjectionAnnotationsPredicate(),
118+
); err != nil {
119+
return err
120+
}
121+
122+
if err := ctrlr.Watch(
123+
source.NewKindWithCache(&corev1.Secret{}, r.Cache),
124+
secretToConfigMapMapper(r.Client),
125+
); err != nil {
126+
return err
127+
}
128+
return nil
129+
}
130+
131+
func configMapHasInjectionAnnotationsPredicate() predicate.Predicate {
132+
return predicate.NewPredicateFuncs(func(object client.Object) bool {
133+
cm := object.(*corev1.ConfigMap)
134+
if _, ok := cm.Annotations[configMapInjectFromSecretName]; !ok {
135+
return false
136+
}
137+
if _, ok := cm.Annotations[configMapInjectFromSecretKey]; !ok {
138+
return false
139+
}
140+
return true
141+
})
142+
}
143+
144+
func secretToConfigMapMapper(cl client.Reader) handler.EventHandler {
145+
return handler.EnqueueRequestsFromMapFunc(func(object client.Object) []reconcile.Request {
146+
cmList := &corev1.ConfigMapList{}
147+
if err := cl.List(context.TODO(), cmList, client.InNamespace(object.GetNamespace())); err != nil {
148+
return nil
149+
}
150+
reqs := []reconcile.Request{}
151+
for _, cm := range cmList.Items {
152+
if cm.Annotations[configMapInjectFromSecretName] == object.GetName() {
153+
reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&cm)})
154+
}
155+
}
156+
return reqs
157+
})
158+
}

internal/rukpakctl/ca.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import (
1313

1414
// GetClusterCA returns an x509.CertPool by reading the contents of a Kubernetes Secret. It uses the provided
1515
// client to get the requested secret and then loads the contents of the secret's "ca.crt" key into the cert pool.
16-
func GetClusterCA(ctx context.Context, cl client.Reader, secretKey types.NamespacedName) (*x509.CertPool, error) {
17-
caSecret := &corev1.Secret{}
18-
if err := cl.Get(ctx, secretKey, caSecret); err != nil {
16+
func GetClusterCA(ctx context.Context, cl client.Reader, configmapKey types.NamespacedName) (*x509.CertPool, error) {
17+
caConfigMap := &corev1.ConfigMap{}
18+
if err := cl.Get(ctx, configmapKey, caConfigMap); err != nil {
1919
return nil, fmt.Errorf("get rukpak certificate authority: %v", err)
2020
}
2121
certPool := x509.NewCertPool()
22-
if !certPool.AppendCertsFromPEM(caSecret.Data["ca.crt"]) {
22+
if !certPool.AppendCertsFromPEM([]byte(caConfigMap.Data["ca-bundle.crt"])) {
2323
return nil, errors.New("failed to load certificate authority into cert pool: malformed PEM?")
2424
}
2525
return certPool, nil

manifests/core/resources/rukpak_issuer.yaml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,14 @@ metadata:
3434
namespace: rukpak-system
3535
spec:
3636
ca:
37-
secretName: rukpak-ca
37+
secretName: rukpak-ca
38+
---
39+
apiVersion: v1
40+
kind: ConfigMap
41+
metadata:
42+
annotations:
43+
core.rukpak.io/inject-from-secret-name: rukpak-ca
44+
core.rukpak.io/inject-from-secret-key: tls.crt
45+
core.rukpak.io/inject-to-data-key: ca-bundle.crt
46+
name: rukpak-ca
47+
namespace: rukpak-system

test/e2e/configmap_syncer_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package e2e
2+
3+
import (
4+
"context"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
corev1 "k8s.io/api/core/v1"
9+
"k8s.io/apimachinery/pkg/types"
10+
)
11+
12+
var _ = Describe("ConfigMapSyncer", func() {
13+
ctx := context.Background()
14+
It("should populate rukpak-ca configmap", func() {
15+
By("fetching rukpak-ca secret")
16+
secret := &corev1.Secret{}
17+
Eventually(func() (map[string][]byte, error) {
18+
err := c.Get(ctx, types.NamespacedName{Namespace: defaultSystemNamespace, Name: "rukpak-ca"}, secret)
19+
return secret.Data, err
20+
}).Should(And(
21+
HaveKey("ca.crt"),
22+
HaveKey("tls.crt"),
23+
HaveKey("tls.key"),
24+
))
25+
26+
By("fetching rukpak-ca configmap")
27+
cm := &corev1.ConfigMap{}
28+
Eventually(func() (map[string]string, error) {
29+
err := c.Get(ctx, types.NamespacedName{Namespace: defaultSystemNamespace, Name: "rukpak-ca"}, cm)
30+
return cm.Data, err
31+
}).Should(HaveKey("ca-bundle.crt"))
32+
33+
By("comparing expected injected value")
34+
Expect(string(secret.Data["tls.crt"])).To(Equal(cm.Data["ca-bundle.crt"]))
35+
})
36+
})

0 commit comments

Comments
 (0)