Skip to content

Commit 70abe68

Browse files
committed
feat: inject bundle data into configmap
Signed-off-by: Erik Godding Boye <egboye@gmail.com>
1 parent 834957c commit 70abe68

File tree

3 files changed

+399
-0
lines changed

3 files changed

+399
-0
lines changed

pkg/bundle/inject/controller.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
Copyright 2021 The cert-manager Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package inject
18+
19+
import (
20+
"context"
21+
"crypto/sha256"
22+
"encoding/hex"
23+
"encoding/json"
24+
"fmt"
25+
26+
corev1 "k8s.io/api/core/v1"
27+
apierrors "k8s.io/apimachinery/pkg/api/errors"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/apimachinery/pkg/types"
30+
v1 "k8s.io/client-go/applyconfigurations/core/v1"
31+
ctrl "sigs.k8s.io/controller-runtime"
32+
"sigs.k8s.io/controller-runtime/pkg/builder"
33+
"sigs.k8s.io/controller-runtime/pkg/client"
34+
"sigs.k8s.io/controller-runtime/pkg/predicate"
35+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
36+
37+
"github.com/cert-manager/trust-manager/pkg/apis/trust/v1alpha1"
38+
"github.com/cert-manager/trust-manager/pkg/bundle/controller"
39+
"github.com/cert-manager/trust-manager/pkg/bundle/internal/source"
40+
"github.com/cert-manager/trust-manager/pkg/bundle/internal/ssa_client"
41+
)
42+
43+
const (
44+
// BundleInjectBundleNameLabelKey is the key of the label that will trigger the injection of bundle data into the resource.
45+
// The label value should be the name of the bundle to inject data from.
46+
BundleInjectBundleNameLabelKey = "inject.trust-manager.io/bundle-name"
47+
// BundleInjectKeyLabelKey is the key for an optional label to specify the key to inject the bundle data into the resource.
48+
// The bundle data will be injected into the 'ca-bundle.crt' key if this label is not found in resource.
49+
BundleInjectKeyLabelKey = "inject.trust-manager.io/key"
50+
)
51+
52+
type Injector struct {
53+
client.Client
54+
bundleBuilder *source.BundleBuilder
55+
}
56+
57+
func (i *Injector) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
58+
i.bundleBuilder = &source.BundleBuilder{
59+
Reader: mgr.GetClient(),
60+
Options: opts,
61+
}
62+
63+
return ctrl.NewControllerManagedBy(mgr).
64+
Named("configmap-injector").
65+
For(&metav1.PartialObjectMetadata{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"}},
66+
builder.WithPredicates(
67+
hasLabelPredicate(BundleInjectBundleNameLabelKey),
68+
)).
69+
Complete(i)
70+
}
71+
72+
func (i *Injector) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
73+
target := &metav1.PartialObjectMetadata{
74+
TypeMeta: metav1.TypeMeta{
75+
APIVersion: "v1",
76+
Kind: "ConfigMap",
77+
},
78+
}
79+
80+
if err := i.Get(ctx, request.NamespacedName, target); err != nil {
81+
if apierrors.IsNotFound(err) {
82+
return reconcile.Result{}, nil
83+
}
84+
return reconcile.Result{}, err
85+
}
86+
87+
bundleName := target.GetLabels()[BundleInjectBundleNameLabelKey]
88+
if bundleName == "" {
89+
return reconcile.Result{}, nil
90+
}
91+
92+
bundle := &v1alpha1.Bundle{}
93+
if err := i.Get(ctx, types.NamespacedName{Name: bundleName}, bundle); err != nil {
94+
return reconcile.Result{}, fmt.Errorf("failed to look up bundle %q: %w", bundleName, err)
95+
}
96+
97+
// TODO: Add support for additional formats
98+
bundleData, err := i.bundleBuilder.BuildBundle(ctx, bundle.Spec.Sources, nil)
99+
if err != nil {
100+
return reconcile.Result{}, fmt.Errorf("failed to build bundle %q: %w", bundleName, err)
101+
}
102+
key := target.GetLabels()[BundleInjectKeyLabelKey]
103+
if key == "" {
104+
key = "ca-bundle.crt"
105+
}
106+
107+
applyConfig := v1.ConfigMap(request.Name, request.Namespace).
108+
WithAnnotations(map[string]string{
109+
v1alpha1.BundleHashAnnotationKey: trustBundleHash([]byte(bundleData.Data)),
110+
}).
111+
WithData(map[string]string{key: bundleData.Data})
112+
113+
return reconcile.Result{}, patchConfigMap(ctx, i.Client, applyConfig)
114+
}
115+
116+
func trustBundleHash(data []byte) string {
117+
hash := sha256.New()
118+
_, _ = hash.Write(data)
119+
hashValue := [32]byte{}
120+
hash.Sum(hashValue[:0])
121+
dataHash := hex.EncodeToString(hashValue[:])
122+
return dataHash
123+
}
124+
125+
type Cleaner struct {
126+
client.Client
127+
}
128+
129+
func (c *Cleaner) SetupWithManager(mgr ctrl.Manager) error {
130+
return ctrl.NewControllerManagedBy(mgr).
131+
Named("configmap-injector-cleaner").
132+
For(&metav1.PartialObjectMetadata{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"}},
133+
builder.WithPredicates(
134+
hasAnnotationPredicate(v1alpha1.BundleHashAnnotationKey),
135+
predicate.Not(hasLabelPredicate(BundleInjectBundleNameLabelKey)),
136+
)).
137+
Complete(c)
138+
}
139+
140+
func (c *Cleaner) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
141+
applyConfig := v1.ConfigMap(request.Name, request.Namespace)
142+
143+
return reconcile.Result{}, patchConfigMap(ctx, c.Client, applyConfig)
144+
}
145+
146+
func patchConfigMap(ctx context.Context, c client.Client, applyConfig *v1.ConfigMapApplyConfiguration) error {
147+
obj := &corev1.ConfigMap{
148+
ObjectMeta: metav1.ObjectMeta{
149+
Name: *applyConfig.Name,
150+
Namespace: *applyConfig.Namespace,
151+
},
152+
}
153+
154+
encodedPatch, err := json.Marshal(applyConfig)
155+
if err != nil {
156+
return err
157+
}
158+
159+
return c.Patch(ctx, obj, ssa_client.ApplyPatch{Patch: encodedPatch}, ssa_client.FieldManager, client.ForceOwnership)
160+
}
161+
162+
func hasLabelPredicate(key string) predicate.Predicate {
163+
return predicate.NewPredicateFuncs(func(obj client.Object) bool {
164+
_, ok := obj.GetLabels()[key]
165+
return ok
166+
})
167+
}
168+
169+
func hasAnnotationPredicate(key string) predicate.Predicate {
170+
return predicate.NewPredicateFuncs(func(obj client.Object) bool {
171+
_, ok := obj.GetAnnotations()[key]
172+
return ok
173+
})
174+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
Copyright 2021 The cert-manager Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package inject
18+
19+
import (
20+
corev1 "k8s.io/api/core/v1"
21+
"sigs.k8s.io/controller-runtime/pkg/envtest/komega"
22+
23+
"github.com/cert-manager/trust-manager/pkg/bundle/inject"
24+
"github.com/cert-manager/trust-manager/test/dummy"
25+
26+
. "github.com/onsi/ginkgo/v2"
27+
. "github.com/onsi/gomega"
28+
)
29+
30+
var _ = Describe("Injector", func() {
31+
var namespace string
32+
33+
BeforeEach(func() {
34+
ns := &corev1.Namespace{}
35+
ns.GenerateName = "inject-"
36+
Expect(k8sClient.Create(ctx, ns)).To(Succeed())
37+
namespace = ns.Name
38+
})
39+
40+
It("should inject bundle data when ConfigMap labeled", func() {
41+
cm := &corev1.ConfigMap{}
42+
cm.GenerateName = "cm-"
43+
cm.Namespace = namespace
44+
cm.Labels = map[string]string{
45+
inject.BundleInjectBundleNameLabelKey: bundleName,
46+
"app": "my-app",
47+
}
48+
cm.Data = map[string]string{
49+
"tls.crt": "bar",
50+
"tls.key": "baz",
51+
}
52+
Expect(k8sClient.Create(ctx, cm)).To(Succeed())
53+
54+
// Wait for ConfigMap to be processed by controller
55+
Eventually(komega.Object(cm)).Should(
56+
HaveField("Data",
57+
HaveKeyWithValue("ca-bundle.crt", dummy.TestCertificate1),
58+
),
59+
)
60+
Expect(cm.Labels).To(HaveKeyWithValue("app", "my-app"))
61+
62+
By("changing key label on ConfigMap, it should switch key", func() {
63+
Expect(komega.Update(cm, func() {
64+
cm.Labels[inject.BundleInjectKeyLabelKey] = "ca.crt"
65+
})()).To(Succeed())
66+
67+
// Wait for ConfigMap to be processed by controller
68+
Eventually(komega.Object(cm)).Should(
69+
HaveField("Data", SatisfyAll(
70+
HaveKeyWithValue("ca.crt", dummy.TestCertificate1),
71+
Not(HaveKey("ca-bundle.crt")),
72+
)),
73+
)
74+
})
75+
76+
By("removing label from ConfigMap, it should remove bundle data", func() {
77+
Expect(komega.Update(cm, func() {
78+
delete(cm.Labels, inject.BundleInjectBundleNameLabelKey)
79+
})()).To(Succeed())
80+
81+
// Wait for ConfigMap to be processed by controller
82+
Eventually(komega.Object(cm)).Should(
83+
HaveField("Data",
84+
Not(HaveKey("ca.crt")),
85+
),
86+
)
87+
Expect(cm.Labels).To(HaveKeyWithValue("app", "my-app"))
88+
Expect(cm.Data).To(Equal(map[string]string{
89+
"tls.crt": "bar",
90+
"tls.key": "baz",
91+
}))
92+
})
93+
})
94+
})

0 commit comments

Comments
 (0)