From 6b4ef47e97e6a52351adb7586f8153cc3b13aee7 Mon Sep 17 00:00:00 2001 From: Erik Godding Boye Date: Fri, 19 Jul 2024 00:38:44 +0200 Subject: [PATCH] feat: inject bundle data into configmap Signed-off-by: Erik Godding Boye --- pkg/bundle/inject/controller.go | 111 ++++++++++++++++++ .../bundle/inject/controller_test.go | 75 ++++++++++++ test/integration/bundle/inject/suite_test.go | 105 +++++++++++++++++ 3 files changed, 291 insertions(+) create mode 100644 pkg/bundle/inject/controller.go create mode 100644 test/integration/bundle/inject/controller_test.go create mode 100644 test/integration/bundle/inject/suite_test.go diff --git a/pkg/bundle/inject/controller.go b/pkg/bundle/inject/controller.go new file mode 100644 index 00000000..42f3dbad --- /dev/null +++ b/pkg/bundle/inject/controller.go @@ -0,0 +1,111 @@ +/* +Copyright 2021 The cert-manager Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package inject + +import ( + "context" + "crypto/sha256" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/client-go/applyconfigurations/core/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/cert-manager/trust-manager/pkg/apis/trust/v1alpha1" + "github.com/cert-manager/trust-manager/pkg/bundle/internal/ssa_client" +) + +const ( + BundleInjectLabelKey = "trust-manager.io/inject-bundle" + + fieldManager = "trust-manager-injector" +) + +var configMap = &metav1.PartialObjectMetadata{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"}} + +type Injector struct { + client.Client +} + +func (i *Injector) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + Named("configmap-injector"). + For(configMap, + builder.WithPredicates( + hasLabel(BundleInjectLabelKey), + )). + Complete(i) +} + +func (i *Injector) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { + data := map[string]string{"ca.crt": "bundle data"} + dataHash := fmt.Sprintf("%x", sha256.Sum256([]byte("bundle data hash"))) + + applyConfig := v1.ConfigMap(request.Name, request.Namespace). + WithAnnotations(map[string]string{v1alpha1.BundleHashAnnotationKey: dataHash}). + WithData(data) + + return reconcile.Result{}, patchConfigMap(ctx, i.Client, applyConfig) +} + +type Cleaner struct { + client.Client +} + +func (c *Cleaner) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + Named("configmap-injector-cleaner"). + For(configMap, + builder.WithPredicates( + hasAnnotation(v1alpha1.BundleHashAnnotationKey), + predicate.Not(hasLabel(BundleInjectLabelKey)), + )). + Complete(c) +} + +func (c *Cleaner) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { + applyConfig := v1.ConfigMap(request.Name, request.Namespace) + + return reconcile.Result{}, patchConfigMap(ctx, c.Client, applyConfig) +} + +func patchConfigMap(ctx context.Context, c client.Client, applyConfig *v1.ConfigMapApplyConfiguration) error { + configMap, patch, err := ssa_client.GenerateConfigMapPatch(applyConfig) + if err != nil { + return err + } + + return c.Patch(ctx, configMap, patch, client.FieldOwner(fieldManager), client.ForceOwnership) +} + +func hasLabel(key string) predicate.Predicate { + return predicate.NewPredicateFuncs(func(obj client.Object) bool { + _, ok := obj.GetLabels()[key] + return ok + }) +} + +func hasAnnotation(key string) predicate.Predicate { + return predicate.NewPredicateFuncs(func(obj client.Object) bool { + _, ok := obj.GetAnnotations()[key] + return ok + }) +} diff --git a/test/integration/bundle/inject/controller_test.go b/test/integration/bundle/inject/controller_test.go new file mode 100644 index 00000000..a42a2b8e --- /dev/null +++ b/test/integration/bundle/inject/controller_test.go @@ -0,0 +1,75 @@ +/* +Copyright 2021 The cert-manager Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package inject + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/envtest/komega" + + "github.com/cert-manager/trust-manager/pkg/bundle/inject" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Injector", func() { + var namespace string + + BeforeEach(func() { + ctx = context.Background() + + ns := &corev1.Namespace{} + ns.GenerateName = "inject-" + Expect(k8sClient.Create(ctx, ns)).To(Succeed()) + namespace = ns.Name + }) + + It("should inject bundle data when ConfigMap labeled", func() { + cm := &corev1.ConfigMap{} + cm.GenerateName = "cm-" + cm.Namespace = namespace + cm.Labels = map[string]string{ + inject.BundleInjectLabelKey: "foo-bundle", + "app": "my-app", + } + cm.Data = map[string]string{ + "tls.crt": "bar", + "tls.key": "baz", + } + Expect(k8sClient.Create(ctx, cm)).To(Succeed()) + + // Wait for ConfigMap to be processed by controller + Eventually(komega.Object(cm)).Should(HaveField("Data", HaveKeyWithValue("ca.crt", "bundle data"))) + Expect(cm.Labels).To(HaveKeyWithValue("app", "my-app")) + + By("removing label from ConfigMap, it should remove bundle data", func() { + Expect(komega.Update(cm, func() { + delete(cm.Labels, inject.BundleInjectLabelKey) + })()).To(Succeed()) + + // Wait for ConfigMap to be processed by controller + Eventually(komega.Object(cm)).Should(HaveField("Data", Not(HaveKey("ca.crt")))) + Expect(cm.Labels).To(HaveKeyWithValue("app", "my-app")) + Expect(cm.Data).To(Equal(map[string]string{ + "tls.crt": "bar", + "tls.key": "baz", + })) + }) + }) +}) diff --git a/test/integration/bundle/inject/suite_test.go b/test/integration/bundle/inject/suite_test.go new file mode 100644 index 00000000..db72cb44 --- /dev/null +++ b/test/integration/bundle/inject/suite_test.go @@ -0,0 +1,105 @@ +/* +Copyright 2021 The cert-manager Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package inject + +import ( + "context" + "testing" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/envtest/komega" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + "github.com/cert-manager/trust-manager/pkg/bundle/inject" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + ctx context.Context + cancel context.CancelFunc +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{} + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + komega.SetClient(k8sClient) + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Client: client.Options{Cache: &client.CacheOptions{Unstructured: true}}, + Scheme: scheme.Scheme, + Metrics: server.Options{ + // Disable metrics server to avoid port conflict + BindAddress: "0", + }, + }) + Expect(err).NotTo(HaveOccurred()) + + injector := &inject.Injector{ + Client: k8sManager.GetClient(), + } + Expect(injector.SetupWithManager(k8sManager)).To(Succeed()) + cleaner := &inject.Cleaner{ + Client: k8sManager.GetClient(), + } + Expect(cleaner.SetupWithManager(k8sManager)).To(Succeed()) + + go func() { + defer GinkgoRecover() + var ctrlCtx context.Context + ctrlCtx, cancel = context.WithCancel(ctrl.SetupSignalHandler()) + Expect(k8sManager.Start(ctrlCtx)).To(Succeed()) + }() +}) + +var _ = AfterSuite(func() { + cancel() + + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +})