Skip to content

Commit bbe9e97

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

File tree

3 files changed

+396
-0
lines changed

3 files changed

+396
-0
lines changed

pkg/bundle/inject/controller.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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+
var configMap = &metav1.PartialObjectMetadata{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"}}
53+
54+
type Injector struct {
55+
client.Client
56+
bundleBuilder *source.BundleBuilder
57+
}
58+
59+
func (i *Injector) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
60+
i.bundleBuilder = &source.BundleBuilder{
61+
Reader: mgr.GetClient(),
62+
Options: opts,
63+
}
64+
65+
return ctrl.NewControllerManagedBy(mgr).
66+
Named("configmap-injector").
67+
For(configMap,
68+
builder.WithPredicates(
69+
hasLabelPredicate(BundleInjectBundleNameLabelKey),
70+
)).
71+
Complete(i)
72+
}
73+
74+
func (i *Injector) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
75+
target := configMap.DeepCopy()
76+
77+
if err := i.Get(ctx, request.NamespacedName, target); err != nil {
78+
if apierrors.IsNotFound(err) {
79+
return reconcile.Result{}, nil
80+
}
81+
return reconcile.Result{}, err
82+
}
83+
84+
bundleName := target.GetLabels()[BundleInjectBundleNameLabelKey]
85+
if bundleName == "" {
86+
return reconcile.Result{}, nil
87+
}
88+
89+
bundle := &v1alpha1.Bundle{}
90+
if err := i.Get(ctx, types.NamespacedName{Name: bundleName}, bundle); err != nil {
91+
return reconcile.Result{}, fmt.Errorf("failed to look up bundle %q: %w", bundleName, err)
92+
}
93+
94+
// TODO: Add support for additional formats
95+
bundleData, err := i.bundleBuilder.BuildBundle(ctx, bundle.Spec.Sources, nil)
96+
if err != nil {
97+
return reconcile.Result{}, fmt.Errorf("failed to build bundle %q: %w", bundleName, err)
98+
}
99+
key := target.GetLabels()[BundleInjectKeyLabelKey]
100+
if key == "" {
101+
key = "ca-bundle.crt"
102+
}
103+
104+
applyConfig := v1.ConfigMap(request.Name, request.Namespace).
105+
WithAnnotations(map[string]string{
106+
v1alpha1.BundleHashAnnotationKey: trustBundleHash([]byte(bundleData.Data)),
107+
}).
108+
WithData(map[string]string{key: bundleData.Data})
109+
110+
return reconcile.Result{}, patchConfigMap(ctx, i.Client, applyConfig)
111+
}
112+
113+
func trustBundleHash(data []byte) string {
114+
hash := sha256.New()
115+
_, _ = hash.Write(data)
116+
hashValue := [32]byte{}
117+
hash.Sum(hashValue[:0])
118+
dataHash := hex.EncodeToString(hashValue[:])
119+
return dataHash
120+
}
121+
122+
type Cleaner struct {
123+
client.Client
124+
}
125+
126+
func (c *Cleaner) SetupWithManager(mgr ctrl.Manager) error {
127+
return ctrl.NewControllerManagedBy(mgr).
128+
Named("configmap-injector-cleaner").
129+
For(configMap,
130+
builder.WithPredicates(
131+
hasAnnotationPredicate(v1alpha1.BundleHashAnnotationKey),
132+
predicate.Not(hasLabelPredicate(BundleInjectBundleNameLabelKey)),
133+
)).
134+
Complete(c)
135+
}
136+
137+
func (c *Cleaner) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
138+
applyConfig := v1.ConfigMap(request.Name, request.Namespace)
139+
140+
return reconcile.Result{}, patchConfigMap(ctx, c.Client, applyConfig)
141+
}
142+
143+
func patchConfigMap(ctx context.Context, c client.Client, applyConfig *v1.ConfigMapApplyConfiguration) error {
144+
obj := &corev1.ConfigMap{
145+
ObjectMeta: metav1.ObjectMeta{
146+
Name: *applyConfig.Name,
147+
Namespace: *applyConfig.Namespace,
148+
},
149+
}
150+
151+
encodedPatch, err := json.Marshal(applyConfig)
152+
if err != nil {
153+
return err
154+
}
155+
156+
return c.Patch(ctx, obj, ssa_client.ApplyPatch{Patch: encodedPatch}, ssa_client.FieldManager, client.ForceOwnership)
157+
}
158+
159+
func hasLabelPredicate(key string) predicate.Predicate {
160+
return predicate.NewPredicateFuncs(func(obj client.Object) bool {
161+
_, ok := obj.GetLabels()[key]
162+
return ok
163+
})
164+
}
165+
166+
func hasAnnotationPredicate(key string) predicate.Predicate {
167+
return predicate.NewPredicateFuncs(func(obj client.Object) bool {
168+
_, ok := obj.GetAnnotations()[key]
169+
return ok
170+
})
171+
}
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+
})
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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+
"path"
22+
"testing"
23+
24+
"k8s.io/client-go/rest"
25+
"k8s.io/utils/ptr"
26+
ctrl "sigs.k8s.io/controller-runtime"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
"sigs.k8s.io/controller-runtime/pkg/envtest"
29+
"sigs.k8s.io/controller-runtime/pkg/envtest/komega"
30+
logf "sigs.k8s.io/controller-runtime/pkg/log"
31+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
32+
"sigs.k8s.io/controller-runtime/pkg/metrics/server"
33+
34+
trustapi "github.com/cert-manager/trust-manager/pkg/apis/trust/v1alpha1"
35+
"github.com/cert-manager/trust-manager/pkg/bundle/controller"
36+
"github.com/cert-manager/trust-manager/pkg/bundle/inject"
37+
"github.com/cert-manager/trust-manager/test/dummy"
38+
39+
. "github.com/onsi/ginkgo/v2"
40+
. "github.com/onsi/gomega"
41+
)
42+
43+
const bundleName = "my-bundle"
44+
45+
var (
46+
cfg *rest.Config
47+
k8sClient client.Client
48+
testEnv *envtest.Environment
49+
ctx context.Context
50+
cancel context.CancelFunc
51+
)
52+
53+
func TestAPIs(t *testing.T) {
54+
ctx = t.Context()
55+
RegisterFailHandler(Fail)
56+
RunSpecs(t, "Controller Suite")
57+
}
58+
59+
var _ = BeforeSuite(func() {
60+
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
61+
62+
_, cancel = context.WithCancel(ctx)
63+
64+
By("bootstrapping test environment")
65+
testEnv = &envtest.Environment{
66+
UseExistingCluster: ptr.To(false),
67+
CRDDirectoryPaths: []string{
68+
path.Join("..", "..", "..", "..", "deploy", "crds"),
69+
},
70+
ErrorIfCRDPathMissing: true,
71+
Scheme: trustapi.GlobalScheme,
72+
}
73+
74+
var err error
75+
// cfg is defined in this file globally.
76+
cfg, err = testEnv.Start()
77+
Expect(err).NotTo(HaveOccurred())
78+
Expect(cfg).NotTo(BeNil())
79+
80+
k8sClient, err = client.New(cfg, client.Options{Scheme: trustapi.GlobalScheme})
81+
Expect(err).NotTo(HaveOccurred())
82+
Expect(k8sClient).NotTo(BeNil())
83+
komega.SetClient(k8sClient)
84+
85+
setupBundle()
86+
87+
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
88+
Client: client.Options{Cache: &client.CacheOptions{Unstructured: true}},
89+
Scheme: trustapi.GlobalScheme,
90+
Metrics: server.Options{
91+
// Disable metrics server to avoid port conflict
92+
BindAddress: "0",
93+
},
94+
})
95+
Expect(err).NotTo(HaveOccurred())
96+
97+
injector := &inject.Injector{
98+
Client: k8sManager.GetClient(),
99+
}
100+
Expect(injector.SetupWithManager(k8sManager, controller.Options{})).To(Succeed())
101+
cleaner := &inject.Cleaner{
102+
Client: k8sManager.GetClient(),
103+
}
104+
Expect(cleaner.SetupWithManager(k8sManager)).To(Succeed())
105+
106+
go func() {
107+
defer GinkgoRecover()
108+
var ctrlCtx context.Context
109+
ctrlCtx, cancel = context.WithCancel(ctrl.SetupSignalHandler())
110+
Expect(k8sManager.Start(ctrlCtx)).To(Succeed())
111+
}()
112+
})
113+
114+
var _ = AfterSuite(func() {
115+
cancel()
116+
117+
By("tearing down the test environment")
118+
err := testEnv.Stop()
119+
Expect(err).NotTo(HaveOccurred())
120+
})
121+
122+
func setupBundle() {
123+
bundle := &trustapi.Bundle{}
124+
bundle.Name = bundleName
125+
bundle.Spec.Sources = []trustapi.BundleSource{{
126+
InLine: ptr.To(dummy.TestCertificate1),
127+
}}
128+
129+
err := k8sClient.Create(ctx, bundle)
130+
Expect(err).NotTo(HaveOccurred())
131+
}

0 commit comments

Comments
 (0)