diff --git a/go.mod b/go.mod index c5509ad5f..cc8c4e21f 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( k8s.io/kubernetes v1.34.4 k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 sigs.k8s.io/controller-runtime v0.22.4 - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 sigs.k8s.io/yaml v1.6.0 ) diff --git a/go.sum b/go.sum index d99b37976..efd3c6a19 100644 --- a/go.sum +++ b/go.sum @@ -392,7 +392,7 @@ sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96 h sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96/go.mod h1:EOBQyBowOUsd7U4CJnMHNE0ri+zCXyouGdLwC/jZU+I= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/go.work.sum b/go.work.sum index de0f9daf9..b3e89efaf 100644 --- a/go.work.sum +++ b/go.work.sum @@ -7,6 +7,7 @@ cloud.google.com/go/translate v1.10.3/go.mod h1:GW0vC1qvPtd3pgtypCv4k4U8B7EdgK9/ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA= @@ -64,6 +65,8 @@ github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSY github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go index 788700ef3..66aaa1d4e 100644 --- a/test/e2e/suite_test.go +++ b/test/e2e/suite_test.go @@ -23,9 +23,14 @@ import ( operatorv1 "github.com/openshift/client-go/operator/clientset/versioned/typed/operator/v1" routev1 "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1" + corev1 "k8s.io/api/core/v1" + k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + crclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" + + trustapi "github.com/cert-manager/trust-manager/pkg/apis/trust/v1alpha1" ) const ( @@ -55,6 +60,7 @@ var ( routeClient *routev1.RouteV1Client certmanageroperatorclient *certmanoperatorclient.Clientset certmanagerClient *certmanagerclientset.Clientset + bundleClient crclient.Client validOperatorStatusConditions = map[string]opv1.ConditionStatus{ "Available": opv1.ConditionTrue, @@ -153,6 +159,13 @@ var _ = BeforeSuite(func() { certmanagerClient, err = certmanagerclientset.NewForConfig(cfg) Expect(err).NotTo(HaveOccurred()) + By("creating controller-runtime client for Bundle CRs") + bundleScheme := k8sruntime.NewScheme() + Expect(trustapi.AddToScheme(bundleScheme)).NotTo(HaveOccurred()) + Expect(corev1.AddToScheme(bundleScheme)).NotTo(HaveOccurred()) + bundleClient, err = crclient.New(cfg, crclient.Options{Scheme: bundleScheme}) + Expect(err).NotTo(HaveOccurred()) + By("setting defaultNetworkPolicy to true") err = resetCertManagerNetworkPolicyState(context.TODO(), certmanageroperatorclient) Expect(err).NotTo(HaveOccurred()) diff --git a/test/e2e/trustmanager_bundle_test.go b/test/e2e/trustmanager_bundle_test.go new file mode 100644 index 000000000..8a8b3d5e4 --- /dev/null +++ b/test/e2e/trustmanager_bundle_test.go @@ -0,0 +1,830 @@ +//go:build e2e +// +build e2e + +// This file tests end-to-end Bundle CR behavior under various TrustManager configurations. +// Tests are grouped by TrustManager configuration and exercise the full flow from +// Bundle creation through target sync verification. +// +// Group 1 — Default TrustManager (no optional features): +// - Inline source → ConfigMap target (+ target data drift reconciliation) +// - ConfigMap source → ConfigMap target (+ source update propagation) +// - Secret source → ConfigMap target +// - Multiple sources (ConfigMap + Inline) → ConfigMap target +// - Custom metadata (labels/annotations) on target ConfigMaps +// - Namespace selector filtering +// - Inline source update → target re-sync +// - Bundle deletion → target cleanup +// - Negative: Secret target without SecretTargets enabled +// - Negative: useDefaultCAs without DefaultCAPackage enabled +// - Negative: ConfigMap + Secret sources outside trust namespace not synced +// +// Group 2 — SecretTargets enabled: +// - Inline source → Secret target +// - Inline source → ConfigMap + Secret dual targets +// - ConfigMap source → Secret target +// - Secret target data drift reconciliation (tamper → restore) +// - Negative: Bundle name not in authorizedSecrets list +// - Transition: Enabled → Disabled → existing synced Bundle reports SecretTargetsDisabled +// +// Group 3 — DefaultCAPackage enabled: +// - useDefaultCAs → ConfigMap target +// - useDefaultCAs + Inline → ConfigMap target (combined data) +// +// Group 4 — SecretTargets + DefaultCAPackage enabled: +// - useDefaultCAs + Inline → ConfigMap + Secret dual targets +// +// Group 5 — Custom TrustNamespace: +// - ConfigMap source in custom trust namespace → ConfigMap target +// - Secret source in custom trust namespace → ConfigMap target +// - Negative: ConfigMap + Secret sources in default namespace not synced when custom trust namespace is configured +// +// Group 6 — FilterExpiredCertificates enabled: +// - ConfigMap source with valid + expired certs → only valid cert in ConfigMap target +// - Transition to Disabled → same Bundle re-syncs with both certs in target +package e2e + +import ( + "context" + "crypto/x509" + "fmt" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + trustapi "github.com/cert-manager/trust-manager/pkg/apis/trust/v1alpha1" + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + testutils "github.com/openshift/cert-manager-operator/pkg/controller/istiocsr" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + crclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + bundleTargetKey = "ca-bundle.crt" + bundleTestNamespaceLabel = "bundle-e2e-test" + bundleSourceKey = "ca.crt" +) + +var _ = Describe("Bundle", Ordered, Label("Platform:Generic", "Feature:TrustManager", "TechPreview"), func() { + ctx := context.TODO() + + var ( + testNS *corev1.Namespace + testCertPEM1, testCertPEM2, expiredCertPEM string + + originalUnsupportedAddonFeatures string + originalOperatorLogLevel string + ) + + BeforeAll(func() { + By("capturing original UNSUPPORTED_ADDON_FEATURES from subscription before patching") + var err error + originalUnsupportedAddonFeatures, err = getSubscriptionEnvVar(ctx, loader, "UNSUPPORTED_ADDON_FEATURES") + Expect(err).ShouldNot(HaveOccurred()) + + By("capturing original OPERATOR_LOG_LEVEL from subscription before patching") + originalOperatorLogLevel, err = getSubscriptionEnvVar(ctx, loader, "OPERATOR_LOG_LEVEL") + Expect(err).ShouldNot(HaveOccurred()) + + By("enabling TrustManager feature gate via subscription") + err = patchSubscriptionWithEnvVars(ctx, loader, map[string]string{ + "UNSUPPORTED_ADDON_FEATURES": "TrustManager=true", + "OPERATOR_LOG_LEVEL": "4", + }) + Expect(err).ShouldNot(HaveOccurred()) + + By("waiting for operator deployment rollout with feature gate") + err = waitForDeploymentEnvVarAndRollout(ctx, operatorNamespace, operatorDeploymentName, + "UNSUPPORTED_ADDON_FEATURES", "TrustManager=true", highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + + By("generating test certificates") + caTweak := func(cert *x509.Certificate) { + cert.IsCA = true + cert.KeyUsage |= x509.KeyUsageCertSign + } + testCertPEM1 = testutils.GenerateCertificate("e2e-test-ca-1", []string{"cert-manager-operator-e2e"}, caTweak) + testCertPEM2 = testutils.GenerateCertificate("e2e-test-ca-2", []string{"cert-manager-operator-e2e"}, caTweak) + + expiredCATweak := func(cert *x509.Certificate) { + cert.IsCA = true + cert.KeyUsage |= x509.KeyUsageCertSign + cert.NotBefore = time.Now().Add(-48 * time.Hour) + cert.NotAfter = time.Now().Add(-24 * time.Hour) + } + expiredCertPEM = testutils.GenerateCertificate("e2e-expired-ca", []string{"cert-manager-operator-e2e"}, expiredCATweak) + + By("creating test namespace for target verification") + testNS = createNamespaceWithCleanup(ctx, "bundle-e2e-", map[string]string{bundleTestNamespaceLabel: "true"}) + }) + + AfterAll(func() { + By("restoring UNSUPPORTED_ADDON_FEATURES and OPERATOR_LOG_LEVEL on subscription to pre-suite values") + err := patchSubscriptionWithEnvVars(ctx, loader, map[string]string{ + "UNSUPPORTED_ADDON_FEATURES": originalUnsupportedAddonFeatures, + "OPERATOR_LOG_LEVEL": originalOperatorLogLevel, + }) + Expect(err).ShouldNot(HaveOccurred()) + if originalUnsupportedAddonFeatures == "" { + By("waiting for operator deployment to rollout after removing UNSUPPORTED_ADDON_FEATURES") + err = waitForDeploymentEnvVarRemovedAndRollout(ctx, operatorNamespace, operatorDeploymentName, "UNSUPPORTED_ADDON_FEATURES", lowTimeout) + } else { + By("waiting for operator deployment to rollout with restored UNSUPPORTED_ADDON_FEATURES") + err = waitForDeploymentEnvVarAndRollout(ctx, operatorNamespace, operatorDeploymentName, "UNSUPPORTED_ADDON_FEATURES", originalUnsupportedAddonFeatures, lowTimeout) + } + Expect(err).ShouldNot(HaveOccurred()) + }) + + // ===== Group 1: Default TrustManager ===== + Context("with default TrustManager configuration", Ordered, func() { + BeforeAll(func() { createTrustManager(ctx, newTrustManagerCR()) }) + AfterAll(func() { deleteTrustManager(ctx) }) + + It("should sync inline source to ConfigMap target and restore tampered target", func() { + bundleName := "bundle-inline-cm-" + randomStr(5) + bundle := newBundle(bundleName). + WithInLineSource(testCertPEM1). + WithConfigMapTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying ConfigMap target is synced in test namespace") + err := waitForConfigMapTarget(ctx, bundleClient, bundleName, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + + verifyBundleSynced(ctx, bundleName) + + By("tampering with the target ConfigMap data") + targetCM, err := k8sClientSet.CoreV1().ConfigMaps(testNS.Name).Get(ctx, bundleName, metav1.GetOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + targetCM.Data[bundleTargetKey] = "tampered-data" + _, err = k8sClientSet.CoreV1().ConfigMaps(testNS.Name).Update(ctx, targetCM, metav1.UpdateOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + + By("verifying trust-manager restores the target ConfigMap") + err = waitForConfigMapTarget(ctx, bundleClient, bundleName, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("should sync ConfigMap source to ConfigMap target and re-sync on source update", func() { + bundleName := "bundle-cm-src-" + randomStr(5) + sourceCMName := "bundle-source-cm-" + randomStr(5) + + By("creating source ConfigMap in trust namespace") + createSourceConfigMap(ctx, trustManagerNamespace, sourceCMName, bundleSourceKey, testCertPEM1) + + bundle := newBundle(bundleName). + WithConfigMapSource(sourceCMName, bundleSourceKey). + WithConfigMapTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying ConfigMap target contains source data") + err := waitForConfigMapTarget(ctx, bundleClient, bundleName, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + + By("updating source ConfigMap data") + Eventually(func() error { + current, err := k8sClientSet.CoreV1().ConfigMaps(trustManagerNamespace).Get(ctx, sourceCMName, metav1.GetOptions{}) + if err != nil { + return err + } + current.Data[bundleSourceKey] = testCertPEM2 + _, err = k8sClientSet.CoreV1().ConfigMaps(trustManagerNamespace).Update(ctx, current, metav1.UpdateOptions{}) + return err + }, lowTimeout, fastPollInterval).Should(Succeed()) + + By("verifying target reflects updated source data") + err = waitForConfigMapTarget(ctx, bundleClient, bundleName, testNS.Name, bundleTargetKey, testCertPEM2, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("should sync Secret source to ConfigMap target", func() { + bundleName := "bundle-secret-src-" + randomStr(5) + sourceSecretName := "bundle-source-secret-" + randomStr(5) + + By("creating source Secret in trust namespace") + createSourceSecret(ctx, trustManagerNamespace, sourceSecretName, bundleSourceKey, testCertPEM1) + + bundle := newBundle(bundleName). + WithSecretSource(sourceSecretName, bundleSourceKey). + WithConfigMapTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying ConfigMap target contains source Secret data") + err := waitForConfigMapTarget(ctx, bundleClient, bundleName, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("should sync multiple sources to ConfigMap target", func() { + bundleName := "bundle-multi-src-" + randomStr(5) + sourceCMName := "bundle-multi-cm-" + randomStr(5) + + By("creating source ConfigMap in trust namespace") + createSourceConfigMap(ctx, trustManagerNamespace, sourceCMName, bundleSourceKey, testCertPEM1) + + bundle := newBundle(bundleName). + WithConfigMapSource(sourceCMName, bundleSourceKey). + WithInLineSource(testCertPEM2). + WithConfigMapTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying ConfigMap target contains data from all sources") + err := waitForConfigMapTarget(ctx, bundleClient, bundleName, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + err = waitForConfigMapTarget(ctx, bundleClient, bundleName, testNS.Name, bundleTargetKey, testCertPEM2, lowTimeout) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("should apply custom metadata to target ConfigMaps", func() { + bundleName := "bundle-meta-" + randomStr(5) + bundle := newBundle(bundleName). + WithInLineSource(testCertPEM1). + WithConfigMapTarget(bundleTargetKey). + WithTargetMetadata( + map[string]string{"e2e-label": "test-value"}, + map[string]string{"e2e-annotation": "test-value"}, + ). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying target ConfigMap has custom labels and annotations") + Eventually(func(g Gomega) { + cm, err := k8sClientSet.CoreV1().ConfigMaps(testNS.Name).Get(ctx, bundleName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(cm.Labels).Should(HaveKeyWithValue("e2e-label", "test-value")) + g.Expect(cm.Annotations).Should(HaveKeyWithValue("e2e-annotation", "test-value")) + }, highTimeout, fastPollInterval).Should(Succeed()) + }) + + It("should sync only to namespaces matching selector", func() { + bundleName := "bundle-ns-sel-" + randomStr(5) + selectorLabel := "bundle-selector-" + randomStr(5) + + By("creating a matching namespace") + matchNS := createNamespaceWithCleanup(ctx, "bundle-match-", map[string]string{selectorLabel: "true"}) + + By("creating a non-matching namespace") + noMatchNS := createNamespaceWithCleanup(ctx, "bundle-nomatch-", nil) + + bundle := newBundle(bundleName). + WithInLineSource(testCertPEM1). + WithConfigMapTarget(bundleTargetKey). + WithNamespaceSelector(map[string]string{selectorLabel: "true"}). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying ConfigMap exists in matching namespace") + err := waitForConfigMapTarget(ctx, bundleClient, bundleName, matchNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + + By("verifying ConfigMap does NOT exist in non-matching namespace") + Consistently(func() bool { + _, err := k8sClientSet.CoreV1().ConfigMaps(noMatchNS.Name).Get(ctx, bundleName, metav1.GetOptions{}) + return apierrors.IsNotFound(err) + }, "30s", fastPollInterval).Should(BeTrue()) + }) + + It("should update targets when inline source changes", func() { + bundleName := "bundle-update-" + randomStr(5) + bundle := newBundle(bundleName). + WithInLineSource(testCertPEM1). + WithConfigMapTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying initial sync") + err := waitForConfigMapTarget(ctx, bundleClient, bundleName, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + + By("updating Bundle inline source") + Eventually(func() error { + var current trustapi.Bundle + if err := bundleClient.Get(ctx, crclient.ObjectKey{Name: bundleName}, ¤t); err != nil { + return err + } + current.Spec.Sources[0].InLine = &testCertPEM2 + return bundleClient.Update(ctx, ¤t) + }, lowTimeout, fastPollInterval).Should(Succeed()) + + By("verifying target reflects updated data") + err = waitForConfigMapTarget(ctx, bundleClient, bundleName, testNS.Name, bundleTargetKey, testCertPEM2, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("should remove targets when Bundle is deleted", func() { + bundleName := "bundle-delete-" + randomStr(5) + bundle := newBundle(bundleName). + WithInLineSource(testCertPEM1). + WithConfigMapTarget(bundleTargetKey). + Build() + + Expect(bundleClient.Create(ctx, bundle)).ShouldNot(HaveOccurred()) + + By("verifying target is synced") + err := waitForConfigMapTarget(ctx, bundleClient, bundleName, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + + By("deleting the Bundle") + deleteBundle(ctx, bundleName) + + By("verifying targets are removed") + err = waitForTargetRemoved(ctx, bundleClient, bundleName, testNS.Name, lowTimeout) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("should report error in Bundle status when targeting Secret without SecretTargets enabled", func() { + bundleName := "bundle-no-secret-" + randomStr(5) + bundle := newBundle(bundleName). + WithInLineSource(testCertPEM1). + WithSecretTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying Bundle status condition shows not synced") + verifyBundleNeverSynced(ctx, bundleName) + + By("verifying no Secret is created in test namespace") + _, err := k8sClientSet.CoreV1().Secrets(testNS.Name).Get(ctx, bundleName, metav1.GetOptions{}) + Expect(apierrors.IsNotFound(err)).Should(BeTrue()) + }) + + It("should report error in Bundle status when using useDefaultCAs without DefaultCAPackage", func() { + bundleName := "bundle-no-default-ca-" + randomStr(5) + bundle := newBundle(bundleName). + WithUseDefaultCAs(). + WithConfigMapTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying Bundle status does not reach Synced=True") + verifyBundleNeverSynced(ctx, bundleName) + }) + + It("should not sync sources that exist outside the trust namespace", func() { + bundleName := "bundle-wrong-ns-" + randomStr(5) + sourceCMName := "src-wrong-ns-" + randomStr(5) + sourceSecretName := "src-secret-wrong-ns-" + randomStr(5) + + By(fmt.Sprintf("creating source ConfigMap and Secret in test namespace %q instead of trust namespace %q", testNS.Name, trustManagerNamespace)) + createSourceConfigMap(ctx, testNS.Name, sourceCMName, bundleSourceKey, testCertPEM1) + createSourceSecret(ctx, testNS.Name, sourceSecretName, bundleSourceKey, testCertPEM2) + + bundle := newBundle(bundleName). + WithConfigMapSource(sourceCMName, bundleSourceKey). + WithSecretSource(sourceSecretName, bundleSourceKey). + WithConfigMapTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying Bundle does not reach Synced=True because sources are not in trust namespace") + verifyBundleNeverSynced(ctx, bundleName) + + By("verifying no target ConfigMap is created in test namespace") + _, err := k8sClientSet.CoreV1().ConfigMaps(testNS.Name).Get(ctx, bundleName, metav1.GetOptions{}) + Expect(apierrors.IsNotFound(err)).Should(BeTrue()) + }) + }) + + // ===== Group 2: SecretTargets enabled ===== + Context("with SecretTargets enabled", Ordered, func() { + const ( + bundleSecretTarget = "bundle-secret-tgt" + bundleDualTarget = "bundle-dual-tgt" + bundleCMToSecretTarget = "bundle-cm-to-secret" + bundleSecretDrift = "bundle-secret-drift" + bundleSecretDisable = "bundle-secret-disable" + ) + + BeforeAll(func() { + createTrustManager(ctx, newTrustManagerCR().WithSecretTargets( + v1alpha1.SecretTargetsPolicyCustom, + []string{bundleSecretTarget, bundleDualTarget, bundleCMToSecretTarget, bundleSecretDrift, bundleSecretDisable}, + )) + }) + AfterAll(func() { deleteTrustManager(ctx) }) + + It("should sync inline source to Secret target", func() { + bundle := newBundle(bundleSecretTarget). + WithInLineSource(testCertPEM1). + WithSecretTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying Secret target is synced in test namespace") + err := waitForSecretTarget(ctx, bundleClient, bundleSecretTarget, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + + verifyBundleSynced(ctx, bundleSecretTarget) + }) + + It("should sync to both ConfigMap and Secret targets", func() { + bundle := newBundle(bundleDualTarget). + WithInLineSource(testCertPEM1). + WithConfigMapTarget(bundleTargetKey). + WithSecretTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying ConfigMap target is synced") + err := waitForConfigMapTarget(ctx, bundleClient, bundleDualTarget, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + + By("verifying Secret target is synced") + err = waitForSecretTarget(ctx, bundleClient, bundleDualTarget, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("should sync ConfigMap source to Secret target", func() { + sourceCMName := "bundle-cm-secret-src-" + randomStr(5) + + By("creating source ConfigMap in trust namespace") + createSourceConfigMap(ctx, trustManagerNamespace, sourceCMName, bundleSourceKey, testCertPEM1) + + bundle := newBundle(bundleCMToSecretTarget). + WithConfigMapSource(sourceCMName, bundleSourceKey). + WithSecretTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying Secret target contains source ConfigMap data") + err := waitForSecretTarget(ctx, bundleClient, bundleCMToSecretTarget, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + + verifyBundleSynced(ctx, bundleCMToSecretTarget) + }) + + It("should restore Secret target when tampered", func() { + bundle := newBundle(bundleSecretDrift). + WithInLineSource(testCertPEM1). + WithSecretTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying Secret target is synced") + err := waitForSecretTarget(ctx, bundleClient, bundleSecretDrift, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + + By("tampering with the target Secret data") + targetSecret, err := k8sClientSet.CoreV1().Secrets(testNS.Name).Get(ctx, bundleSecretDrift, metav1.GetOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + targetSecret.Data[bundleTargetKey] = []byte("tampered-data") + _, err = k8sClientSet.CoreV1().Secrets(testNS.Name).Update(ctx, targetSecret, metav1.UpdateOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + + By("verifying trust-manager restores the target Secret") + err = waitForSecretTarget(ctx, bundleClient, bundleSecretDrift, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("should not sync Secret when Bundle name is not in authorizedSecrets list", func() { + bundleName := "bundle-not-authorized" + bundle := newBundle(bundleName). + WithInLineSource(testCertPEM1). + WithSecretTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying Bundle status does not reach Synced=True") + verifyBundleNeverSynced(ctx, bundleName) + + By(fmt.Sprintf("verifying no Secret named %q exists in test namespace", bundleName)) + _, err := k8sClientSet.CoreV1().Secrets(testNS.Name).Get(ctx, bundleName, metav1.GetOptions{}) + Expect(apierrors.IsNotFound(err)).Should(BeTrue()) + }) + + It("should report SecretTargetsDisabled on existing synced Bundle after disabling secretTargets", func() { + bundle := newBundle(bundleSecretDisable). + WithInLineSource(testCertPEM1). + WithSecretTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying Secret target syncs while feature is enabled") + err := waitForSecretTarget(ctx, bundleClient, bundleSecretDisable, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + verifyBundleSynced(ctx, bundleSecretDisable) + + By("disabling secretTargets on TrustManager CR") + Eventually(func() error { + tm, err := trustManagerClient().Get(ctx, "cluster", metav1.GetOptions{}) + if err != nil { + return err + } + tm.Spec.TrustManagerConfig.SecretTargets = v1alpha1.SecretTargetsConfig{ + Policy: v1alpha1.SecretTargetsPolicyDisabled, + } + _, err = trustManagerClient().Update(ctx, tm, metav1.UpdateOptions{}) + return err + }, lowTimeout, fastPollInterval).Should(Succeed()) + + waitForTrustManagerReady(ctx) + + By("verifying Bundle status transitions to Synced=False") + err = waitForBundleCondition(ctx, bundleClient, bundleSecretDisable, trustapi.BundleConditionSynced, metav1.ConditionFalse, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + }) + }) + + // ===== Group 3: DefaultCAPackage enabled ===== + Context("with DefaultCAPackage enabled", Ordered, func() { + BeforeAll(func() { + createTrustManager(ctx, newTrustManagerCR().WithDefaultCAPackage(v1alpha1.DefaultCAPackagePolicyEnabled)) + + By("waiting for default CA package ConfigMap to be created") + err := pollTillConfigMapAvailable(ctx, k8sClientSet, trustManagerNamespace, defaultCAPackageConfigMapName) + Expect(err).ShouldNot(HaveOccurred()) + }) + AfterAll(func() { + deleteTrustManager(ctx) + // The operator does not delete managed ConfigMaps when the feature is + // disabled or the CR is removed. Clean up explicitly so subsequent + // tests that assert absence of this ConfigMap start from a clean state. + _ = k8sClientSet.CoreV1().ConfigMaps(trustManagerNamespace).Delete(ctx, defaultCAPackageConfigMapName, metav1.DeleteOptions{}) + }) + + It("should sync useDefaultCAs source to ConfigMap target", func() { + bundleName := "bundle-default-cas-" + randomStr(5) + bundle := newBundle(bundleName). + WithUseDefaultCAs(). + WithConfigMapTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying ConfigMap target contains PEM certificates from default CAs") + Eventually(func(g Gomega) { + cm, err := k8sClientSet.CoreV1().ConfigMaps(testNS.Name).Get(ctx, bundleName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + data, ok := cm.Data[bundleTargetKey] + g.Expect(ok).Should(BeTrue(), "target ConfigMap should contain key %q", bundleTargetKey) + g.Expect(data).ShouldNot(BeEmpty()) + g.Expect(containsPEMCertificates(data)).Should(BeTrue(), "target data should contain valid PEM certificates") + }, highTimeout, slowPollInterval).Should(Succeed()) + + verifyBundleSynced(ctx, bundleName) + }) + + It("should include default CAs alongside explicit inline source", func() { + bundleName := "bundle-cas-inline-" + randomStr(5) + bundle := newBundle(bundleName). + WithInLineSource(testCertPEM1). + WithUseDefaultCAs(). + WithConfigMapTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying target contains the inline certificate") + err := waitForConfigMapTarget(ctx, bundleClient, bundleName, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + + By("verifying target also contains default CA certificates") + Eventually(func(g Gomega) { + cm, err := k8sClientSet.CoreV1().ConfigMaps(testNS.Name).Get(ctx, bundleName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + data := cm.Data[bundleTargetKey] + g.Expect(strings.Contains(data, testCertPEM1)).Should(BeTrue(), "should contain inline cert") + g.Expect(strings.Count(data, "-----BEGIN CERTIFICATE-----")).Should(BeNumerically(">", 1), + "should contain multiple certificates (inline + default CAs)") + }, highTimeout, slowPollInterval).Should(Succeed()) + }) + }) + + // ===== Group 4: Combined SecretTargets + DefaultCAPackage ===== + Context("with SecretTargets and DefaultCAPackage enabled", Ordered, func() { + const bundleCombined = "bundle-combined" + + BeforeAll(func() { + createTrustManager(ctx, newTrustManagerCR(). + WithSecretTargets(v1alpha1.SecretTargetsPolicyCustom, []string{bundleCombined}). + WithDefaultCAPackage(v1alpha1.DefaultCAPackagePolicyEnabled)) + + By("waiting for default CA package ConfigMap to be created") + err := pollTillConfigMapAvailable(ctx, k8sClientSet, trustManagerNamespace, defaultCAPackageConfigMapName) + Expect(err).ShouldNot(HaveOccurred()) + }) + AfterAll(func() { + deleteTrustManager(ctx) + // The operator does not delete managed ConfigMaps when the feature is + // disabled or the CR is removed. Clean up explicitly so subsequent + // tests that assert absence of this ConfigMap start from a clean state. + _ = k8sClientSet.CoreV1().ConfigMaps(trustManagerNamespace).Delete(ctx, defaultCAPackageConfigMapName, metav1.DeleteOptions{}) + }) + + It("should sync useDefaultCAs and inline sources to both ConfigMap and Secret targets", func() { + bundle := newBundle(bundleCombined). + WithInLineSource(testCertPEM1). + WithUseDefaultCAs(). + WithConfigMapTarget(bundleTargetKey). + WithSecretTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying ConfigMap target contains inline cert and default CAs") + Eventually(func(g Gomega) { + cm, err := k8sClientSet.CoreV1().ConfigMaps(testNS.Name).Get(ctx, bundleCombined, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + data := cm.Data[bundleTargetKey] + g.Expect(strings.Contains(data, testCertPEM1)).Should(BeTrue(), "ConfigMap should contain inline cert") + g.Expect(strings.Count(data, "-----BEGIN CERTIFICATE-----")).Should(BeNumerically(">", 1)) + }, highTimeout, slowPollInterval).Should(Succeed()) + + By("verifying Secret target contains inline cert and default CAs") + Eventually(func(g Gomega) { + secret, err := k8sClientSet.CoreV1().Secrets(testNS.Name).Get(ctx, bundleCombined, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + data := string(secret.Data[bundleTargetKey]) + g.Expect(strings.Contains(data, testCertPEM1)).Should(BeTrue(), "Secret should contain inline cert") + g.Expect(strings.Count(data, "-----BEGIN CERTIFICATE-----")).Should(BeNumerically(">", 1)) + }, highTimeout, slowPollInterval).Should(Succeed()) + + verifyBundleSynced(ctx, bundleCombined) + }) + }) + + // ===== Group 5: Custom TrustNamespace ===== + Context("with custom trustNamespace", Ordered, func() { + var customTrustNS *corev1.Namespace + + BeforeAll(func() { + By("creating custom trust namespace") + customTrustNS = createNamespaceWithCleanup(ctx, "custom-trust-ns-", nil) + + createTrustManager(ctx, newTrustManagerCR().WithTrustNamespace(customTrustNS.Name)) + + By(fmt.Sprintf("verifying deployment has --trust-namespace=%s", customTrustNS.Name)) + Eventually(func(g Gomega) { + dep, err := k8sClientSet.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(dep.Spec.Template.Spec.Containers).ShouldNot(BeEmpty()) + g.Expect(dep.Spec.Template.Spec.Containers[0].Args).Should( + ContainElement(fmt.Sprintf("--trust-namespace=%s", customTrustNS.Name)), + ) + }, lowTimeout, fastPollInterval).Should(Succeed()) + }) + + AfterAll(func() { deleteTrustManager(ctx) }) + + It("should sync ConfigMap source from custom trust namespace to target", func() { + bundleName := "bundle-custom-ns-" + randomStr(5) + sourceCMName := "src-custom-ns-" + randomStr(5) + + By(fmt.Sprintf("creating source ConfigMap in custom trust namespace %q", customTrustNS.Name)) + createSourceConfigMap(ctx, customTrustNS.Name, sourceCMName, bundleSourceKey, testCertPEM1) + + bundle := newBundle(bundleName). + WithConfigMapSource(sourceCMName, bundleSourceKey). + WithConfigMapTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying ConfigMap target is synced in test namespace") + err := waitForConfigMapTarget(ctx, bundleClient, bundleName, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + + verifyBundleSynced(ctx, bundleName) + }) + + It("should sync Secret source from custom trust namespace to target", func() { + bundleName := "bundle-secret-custom-ns-" + randomStr(5) + sourceSecretName := "src-secret-custom-ns-" + randomStr(5) + + By(fmt.Sprintf("creating source Secret in custom trust namespace %q", customTrustNS.Name)) + createSourceSecret(ctx, customTrustNS.Name, sourceSecretName, bundleSourceKey, testCertPEM1) + + bundle := newBundle(bundleName). + WithSecretSource(sourceSecretName, bundleSourceKey). + WithConfigMapTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying ConfigMap target is synced in test namespace") + err := waitForConfigMapTarget(ctx, bundleClient, bundleName, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + + verifyBundleSynced(ctx, bundleName) + }) + + It("should not sync sources from default namespace when custom trust namespace is configured", func() { + bundleName := "bundle-default-ns-miss-" + randomStr(5) + sourceCMName := "src-default-ns-miss-" + randomStr(5) + sourceSecretName := "src-secret-default-miss-" + randomStr(5) + + By(fmt.Sprintf("creating source ConfigMap and Secret in default namespace %q (not the custom trust namespace %q)", trustManagerNamespace, customTrustNS.Name)) + createSourceConfigMap(ctx, trustManagerNamespace, sourceCMName, bundleSourceKey, testCertPEM1) + createSourceSecret(ctx, trustManagerNamespace, sourceSecretName, bundleSourceKey, testCertPEM2) + + bundle := newBundle(bundleName). + WithConfigMapSource(sourceCMName, bundleSourceKey). + WithSecretSource(sourceSecretName, bundleSourceKey). + WithConfigMapTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + + By("verifying Bundle does not reach Synced=True because sources are not in custom trust namespace") + verifyBundleNeverSynced(ctx, bundleName) + + By("verifying no target ConfigMap is created in test namespace") + _, err := k8sClientSet.CoreV1().ConfigMaps(testNS.Name).Get(ctx, bundleName, metav1.GetOptions{}) + Expect(apierrors.IsNotFound(err)).Should(BeTrue()) + }) + }) + + // ===== Group 6: FilterExpiredCertificates ===== + Context("with FilterExpiredCertificates enabled", Ordered, func() { + var ( + filterBundleName string + sourceCMName string + ) + + BeforeAll(func() { + createTrustManager(ctx, newTrustManagerCR(). + WithFilterExpiredCertificates(v1alpha1.FilterExpiredCertificatesPolicyEnabled)) + + sourceCMName = "filter-src-cm-" + randomStr(5) + filterBundleName = "bundle-filter-expired-" + randomStr(5) + combinedPEM := testCertPEM1 + expiredCertPEM + + By("creating source ConfigMap with valid + expired certs in trust namespace") + createSourceConfigMap(ctx, trustManagerNamespace, sourceCMName, bundleSourceKey, combinedPEM) + + bundle := newBundle(filterBundleName). + WithConfigMapSource(sourceCMName, bundleSourceKey). + WithConfigMapTarget(bundleTargetKey). + Build() + + createBundleWithCleanup(ctx, bundle) + }) + AfterAll(func() { deleteTrustManager(ctx) }) + + It("should exclude expired certificates from ConfigMap target when using ConfigMap source", func() { + By("verifying target contains the valid certificate") + err := waitForConfigMapTarget(ctx, bundleClient, filterBundleName, testNS.Name, bundleTargetKey, testCertPEM1, highTimeout) + Expect(err).ShouldNot(HaveOccurred()) + + By("verifying target does NOT contain the expired certificate") + Eventually(func(g Gomega) { + cm, err := k8sClientSet.CoreV1().ConfigMaps(testNS.Name).Get(ctx, filterBundleName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + data := cm.Data[bundleTargetKey] + g.Expect(strings.Contains(data, strings.TrimSpace(testCertPEM1))).Should(BeTrue(), "should contain valid cert") + g.Expect(strings.Contains(data, strings.TrimSpace(expiredCertPEM))).Should(BeFalse(), "should not contain expired cert") + }, highTimeout, fastPollInterval).Should(Succeed()) + + verifyBundleSynced(ctx, filterBundleName) + }) + + It("should re-sync same Bundle with expired certs included after disabling filter", func() { + By("disabling filterExpiredCertificates on TrustManager CR") + Eventually(func() error { + tm, err := trustManagerClient().Get(ctx, "cluster", metav1.GetOptions{}) + if err != nil { + return err + } + tm.Spec.TrustManagerConfig.FilterExpiredCertificates = v1alpha1.FilterExpiredCertificatesPolicyDisabled + _, err = trustManagerClient().Update(ctx, tm, metav1.UpdateOptions{}) + return err + }, lowTimeout, fastPollInterval).Should(Succeed()) + + waitForTrustManagerReady(ctx) + + By("verifying the same Bundle's target now includes the expired certificate") + Eventually(func(g Gomega) { + cm, err := k8sClientSet.CoreV1().ConfigMaps(testNS.Name).Get(ctx, filterBundleName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + data := cm.Data[bundleTargetKey] + g.Expect(strings.Contains(data, strings.TrimSpace(testCertPEM1))).Should(BeTrue(), "should contain valid cert") + g.Expect(strings.Contains(data, strings.TrimSpace(expiredCertPEM))).Should(BeTrue(), "should contain expired cert after disabling filter") + }, highTimeout, fastPollInterval).Should(Succeed()) + + verifyBundleSynced(ctx, filterBundleName) + }) + }) +}) diff --git a/test/e2e/trustmanager_helpers_test.go b/test/e2e/trustmanager_helpers_test.go new file mode 100644 index 000000000..4d143f884 --- /dev/null +++ b/test/e2e/trustmanager_helpers_test.go @@ -0,0 +1,362 @@ +//go:build e2e +// +build e2e + +package e2e + +import ( + "context" + "encoding/pem" + "strings" + "time" + + trustapi "github.com/cert-manager/trust-manager/pkg/apis/trust/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + operatorclientv1alpha1 "github.com/openshift/cert-manager-operator/pkg/operator/clientset/versioned/typed/operator/v1alpha1" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/utils/ptr" + crclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +// --------------------------------------------------------------------------- +// TrustManager CR builder +// --------------------------------------------------------------------------- + +type trustManagerCRBuilder struct { + tm *v1alpha1.TrustManager +} + +func newTrustManagerCR() *trustManagerCRBuilder { + return &trustManagerCRBuilder{ + tm: &v1alpha1.TrustManager{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster"}, + Spec: v1alpha1.TrustManagerSpec{ + TrustManagerConfig: v1alpha1.TrustManagerConfig{}, + }, + }, + } +} + +func (b *trustManagerCRBuilder) WithResources(resources corev1.ResourceRequirements) *trustManagerCRBuilder { + b.tm.Spec.TrustManagerConfig.Resources = resources + return b +} + +func (b *trustManagerCRBuilder) WithTolerations(tolerations []corev1.Toleration) *trustManagerCRBuilder { + b.tm.Spec.TrustManagerConfig.Tolerations = tolerations + return b +} + +func (b *trustManagerCRBuilder) WithNodeSelector(nodeSelector map[string]string) *trustManagerCRBuilder { + b.tm.Spec.TrustManagerConfig.NodeSelector = nodeSelector + return b +} + +func (b *trustManagerCRBuilder) WithAffinity(affinity *corev1.Affinity) *trustManagerCRBuilder { + b.tm.Spec.TrustManagerConfig.Affinity = affinity + return b +} + +func (b *trustManagerCRBuilder) WithLabels(labels map[string]string) *trustManagerCRBuilder { + b.tm.Spec.ControllerConfig.Labels = labels + return b +} + +func (b *trustManagerCRBuilder) WithAnnotations(annotations map[string]string) *trustManagerCRBuilder { + b.tm.Spec.ControllerConfig.Annotations = annotations + return b +} + +func (b *trustManagerCRBuilder) WithTrustNamespace(trustNamespace string) *trustManagerCRBuilder { + b.tm.Spec.TrustManagerConfig.TrustNamespace = trustNamespace + return b +} + +func (b *trustManagerCRBuilder) WithSecretTargets(policy v1alpha1.SecretTargetsPolicy, authorizedSecrets []string) *trustManagerCRBuilder { + b.tm.Spec.TrustManagerConfig.SecretTargets = v1alpha1.SecretTargetsConfig{ + Policy: policy, + AuthorizedSecrets: authorizedSecrets, + } + return b +} + +func (b *trustManagerCRBuilder) WithDefaultCAPackage(policy v1alpha1.DefaultCAPackagePolicy) *trustManagerCRBuilder { + b.tm.Spec.TrustManagerConfig.DefaultCAPackage.Policy = policy + return b +} + +func (b *trustManagerCRBuilder) WithFilterExpiredCertificates(policy v1alpha1.FilterExpiredCertificatesPolicy) *trustManagerCRBuilder { + b.tm.Spec.TrustManagerConfig.FilterExpiredCertificates = policy + return b +} + +func (b *trustManagerCRBuilder) Build() *v1alpha1.TrustManager { + return b.tm +} + +// --------------------------------------------------------------------------- +// TrustManager CR helpers +// --------------------------------------------------------------------------- + +func trustManagerClient() operatorclientv1alpha1.TrustManagerInterface { + return certmanageroperatorclient.OperatorV1alpha1().TrustManagers() +} + +func waitForTrustManagerReady(ctx context.Context) v1alpha1.TrustManagerStatus { + By("waiting for TrustManager CR to be ready") + status, err := pollTillTrustManagerAvailable(ctx, trustManagerClient(), "cluster") + Expect(err).Should(BeNil()) + return status +} + +func createTrustManager(ctx context.Context, b *trustManagerCRBuilder) { + By("creating TrustManager CR") + _, err := trustManagerClient().Create(ctx, b.Build(), metav1.CreateOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + waitForTrustManagerReady(ctx) +} + +func deleteTrustManager(ctx context.Context) { + By("deleting TrustManager CR") + _ = trustManagerClient().Delete(ctx, "cluster", metav1.DeleteOptions{}) + Eventually(func() bool { + _, err := trustManagerClient().Get(ctx, "cluster", metav1.GetOptions{}) + return apierrors.IsNotFound(err) + }, lowTimeout, fastPollInterval).Should(BeTrue()) +} + +// --------------------------------------------------------------------------- +// Bundle builder +// --------------------------------------------------------------------------- + +// bundleBuilder provides a fluent API for constructing trust.cert-manager.io/v1alpha1 Bundle objects. +type bundleBuilder struct { + bundle *trustapi.Bundle +} + +func newBundle(name string) *bundleBuilder { + return &bundleBuilder{ + bundle: &trustapi.Bundle{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + Spec: trustapi.BundleSpec{}, + }, + } +} + +func (b *bundleBuilder) WithInLineSource(pemData string) *bundleBuilder { + b.bundle.Spec.Sources = append(b.bundle.Spec.Sources, trustapi.BundleSource{InLine: &pemData}) + return b +} + +func (b *bundleBuilder) WithConfigMapSource(name, key string) *bundleBuilder { + b.bundle.Spec.Sources = append(b.bundle.Spec.Sources, trustapi.BundleSource{ + ConfigMap: &trustapi.SourceObjectKeySelector{Name: name, Key: key}, + }) + return b +} + +func (b *bundleBuilder) WithSecretSource(name, key string) *bundleBuilder { + b.bundle.Spec.Sources = append(b.bundle.Spec.Sources, trustapi.BundleSource{ + Secret: &trustapi.SourceObjectKeySelector{Name: name, Key: key}, + }) + return b +} + +func (b *bundleBuilder) WithUseDefaultCAs() *bundleBuilder { + b.bundle.Spec.Sources = append(b.bundle.Spec.Sources, trustapi.BundleSource{UseDefaultCAs: ptr.To(true)}) + return b +} + +func (b *bundleBuilder) WithConfigMapTarget(key string) *bundleBuilder { + b.bundle.Spec.Target.ConfigMap = &trustapi.TargetTemplate{Key: key} + return b +} + +func (b *bundleBuilder) WithSecretTarget(key string) *bundleBuilder { + b.bundle.Spec.Target.Secret = &trustapi.TargetTemplate{Key: key} + return b +} + +func (b *bundleBuilder) WithTargetMetadata(labels, annotations map[string]string) *bundleBuilder { + meta := &trustapi.TargetMetadata{Labels: labels, Annotations: annotations} + if b.bundle.Spec.Target.ConfigMap != nil { + b.bundle.Spec.Target.ConfigMap.Metadata = meta + } + if b.bundle.Spec.Target.Secret != nil { + b.bundle.Spec.Target.Secret.Metadata = meta + } + return b +} + +func (b *bundleBuilder) WithNamespaceSelector(matchLabels map[string]string) *bundleBuilder { + b.bundle.Spec.Target.NamespaceSelector = &metav1.LabelSelector{MatchLabels: matchLabels} + return b +} + +func (b *bundleBuilder) Build() *trustapi.Bundle { + return b.bundle +} + +// --------------------------------------------------------------------------- +// Bundle helpers +// --------------------------------------------------------------------------- + +// waitForBundleCondition polls until the Bundle has a status condition matching +// the given type and status, or until timeout. +func waitForBundleCondition(ctx context.Context, cl crclient.Client, bundleName, conditionType string, conditionStatus metav1.ConditionStatus, timeout time.Duration) error { + return wait.PollUntilContextTimeout(ctx, fastPollInterval, timeout, true, func(context.Context) (bool, error) { + var bundle trustapi.Bundle + if err := cl.Get(ctx, crclient.ObjectKey{Name: bundleName}, &bundle); err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + return false, err + } + for _, c := range bundle.Status.Conditions { + if c.Type == conditionType && c.Status == conditionStatus { + return true, nil + } + } + return false, nil + }) +} + +// waitForConfigMapTarget polls until a ConfigMap with the Bundle name exists in the +// given namespace and its data key contains the expected content. +func waitForConfigMapTarget(ctx context.Context, cl crclient.Client, bundleName, namespace, key, expectedContent string, timeout time.Duration) error { + return wait.PollUntilContextTimeout(ctx, fastPollInterval, timeout, true, func(context.Context) (bool, error) { + var cm corev1.ConfigMap + if err := cl.Get(ctx, crclient.ObjectKey{Namespace: namespace, Name: bundleName}, &cm); err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + return false, err + } + data, ok := cm.Data[key] + if !ok { + return false, nil + } + if expectedContent != "" { + return strings.Contains(strings.TrimSpace(data), strings.TrimSpace(expectedContent)), nil + } + return len(data) > 0, nil + }) +} + +// waitForSecretTarget polls until a Secret with the Bundle name exists in the +// given namespace and its data key contains the expected content. +func waitForSecretTarget(ctx context.Context, cl crclient.Client, bundleName, namespace, key, expectedContent string, timeout time.Duration) error { + return wait.PollUntilContextTimeout(ctx, fastPollInterval, timeout, true, func(context.Context) (bool, error) { + var secret corev1.Secret + if err := cl.Get(ctx, crclient.ObjectKey{Namespace: namespace, Name: bundleName}, &secret); err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + return false, err + } + data, ok := secret.Data[key] + if !ok { + return false, nil + } + if expectedContent != "" { + return strings.Contains(strings.TrimSpace(string(data)), strings.TrimSpace(expectedContent)), nil + } + return len(data) > 0, nil + }) +} + +// waitForTargetRemoved polls until the target ConfigMap or Secret with the +// Bundle name no longer exists in the given namespace. +func waitForTargetRemoved(ctx context.Context, cl crclient.Client, bundleName, namespace string, timeout time.Duration) error { + return wait.PollUntilContextTimeout(ctx, fastPollInterval, timeout, true, func(context.Context) (bool, error) { + var cm corev1.ConfigMap + cmErr := cl.Get(ctx, crclient.ObjectKey{Namespace: namespace, Name: bundleName}, &cm) + var secret corev1.Secret + secretErr := cl.Get(ctx, crclient.ObjectKey{Namespace: namespace, Name: bundleName}, &secret) + return apierrors.IsNotFound(cmErr) && apierrors.IsNotFound(secretErr), nil + }) +} + +// containsPEMCertificates returns true if the data contains at least one +// valid PEM-encoded CERTIFICATE block. +func containsPEMCertificates(data string) bool { + block, _ := pem.Decode([]byte(data)) + return block != nil && block.Type == "CERTIFICATE" +} + +func deleteBundle(ctx context.Context, name string) { + var bundle trustapi.Bundle + bundle.Name = name + _ = bundleClient.Delete(ctx, &bundle) + Eventually(func() bool { + err := bundleClient.Get(ctx, crclient.ObjectKey{Name: name}, &trustapi.Bundle{}) + return apierrors.IsNotFound(err) + }, lowTimeout, fastPollInterval).Should(BeTrue()) +} + +func createBundleWithCleanup(ctx context.Context, bundle *trustapi.Bundle) { + Eventually(func() error { + return bundleClient.Create(ctx, bundle) + }, lowTimeout, fastPollInterval).Should(Succeed(), "failed to create Bundle %q (webhook may not be ready yet)", bundle.Name) + DeferCleanup(func() { deleteBundle(ctx, bundle.Name) }) +} + +func createNamespaceWithCleanup(ctx context.Context, prefix string, labels map[string]string) *corev1.Namespace { + ns, err := k8sClientSet.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{GenerateName: prefix, Labels: labels}, + }, metav1.CreateOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + DeferCleanup(func() { + _ = k8sClientSet.CoreV1().Namespaces().Delete(ctx, ns.Name, metav1.DeleteOptions{}) + }) + return ns +} + +func createSourceConfigMap(ctx context.Context, namespace, name, key, data string) { + _, err := k8sClientSet.CoreV1().ConfigMaps(namespace).Create(ctx, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + Data: map[string]string{key: data}, + }, metav1.CreateOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + DeferCleanup(func() { + _ = k8sClientSet.CoreV1().ConfigMaps(namespace).Delete(ctx, name, metav1.DeleteOptions{}) + }) +} + +func createSourceSecret(ctx context.Context, namespace, name, key, data string) { + _, err := k8sClientSet.CoreV1().Secrets(namespace).Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + Data: map[string][]byte{key: []byte(data)}, + }, metav1.CreateOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + DeferCleanup(func() { + _ = k8sClientSet.CoreV1().Secrets(namespace).Delete(ctx, name, metav1.DeleteOptions{}) + }) +} + +func verifyBundleSynced(ctx context.Context, bundleName string) { + By("verifying Bundle status shows Synced") + err := waitForBundleCondition(ctx, bundleClient, bundleName, trustapi.BundleConditionSynced, metav1.ConditionTrue, lowTimeout) + Expect(err).ShouldNot(HaveOccurred()) +} + +func verifyBundleNeverSynced(ctx context.Context, bundleName string) { + Consistently(func() bool { + var b trustapi.Bundle + if err := bundleClient.Get(ctx, crclient.ObjectKey{Name: bundleName}, &b); err != nil { + return false + } + for _, c := range b.Status.Conditions { + if c.Type == trustapi.BundleConditionSynced && c.Status == metav1.ConditionTrue { + return false + } + } + return true + }, "60s", fastPollInterval).Should(BeTrue()) +} diff --git a/test/e2e/trustmanager_test.go b/test/e2e/trustmanager_test.go index b749eb483..2fb9c3a4d 100644 --- a/test/e2e/trustmanager_test.go +++ b/test/e2e/trustmanager_test.go @@ -5,13 +5,16 @@ package e2e import ( "context" + "crypto/x509" "fmt" "slices" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + configopenshiftv1 "github.com/openshift/api/config/v1" "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + testutils "github.com/openshift/cert-manager-operator/pkg/controller/istiocsr" operatorclientv1alpha1 "github.com/openshift/cert-manager-operator/pkg/operator/clientset/versioned/typed/operator/v1alpha1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -57,12 +60,6 @@ const ( trustedCABundleKey = "ca-bundle.crt" ) -var ( - trustManagerClient = func() operatorclientv1alpha1.TrustManagerInterface { - return certmanageroperatorclient.OperatorV1alpha1().TrustManagers() - } -) - // Cluster must have an allowed feature set (e.g. TechPreviewNoUpgrade). TrustManager is deployed and reconciled. var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:TrustManager", "TechPreview"), func() { var ( @@ -73,19 +70,6 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru originalOperatorLogLevel string ) - waitForTrustManagerReady := func() v1alpha1.TrustManagerStatus { - By("waiting for TrustManager CR to be ready") - status, err := pollTillTrustManagerAvailable(ctx, trustManagerClient(), "cluster") - Expect(err).Should(BeNil()) - return status - } - - createTrustManager := func(b *trustManagerCRBuilder) { - _, err := trustManagerClient().Create(ctx, b.Build(), metav1.CreateOptions{}) - Expect(err).ShouldNot(HaveOccurred()) - waitForTrustManagerReady() - } - BeforeAll(trustManagerBeforeAll(ctx, &clientset, &originalUnsupportedAddonFeatures, &originalOperatorLogLevel)) AfterAll(trustManagerAfterAll(ctx, &originalUnsupportedAddonFeatures, &originalOperatorLogLevel)) @@ -100,7 +84,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru Context("resource creation", func() { It("should create all resources managed by the controller with correct labels", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) // Namespace-scoped resources By("verifying ServiceAccount") @@ -203,7 +187,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru Context("resource deletion and recreation", func() { It("should recreate resources managed by the controller when deleted externally", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) // Namespace-scoped resources By("deleting and verifying recreation of ServiceAccount") @@ -319,7 +303,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru Context("label drift reconciliation", func() { It("should restore labels when modified externally on managed resources", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) By("modifying ServiceAccount labels externally") sa, err := clientset.CoreV1().ServiceAccounts(trustManagerNamespace).Get(ctx, trustManagerServiceAccountName, metav1.GetOptions{}) @@ -371,7 +355,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru Context("managed label removal reconciliation", func() { It("should restore the managed label when removed externally from resources", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) // The "app" label is the managed resource label used by the predicate // to filter watch events. Removing it tests that the predicate checks @@ -427,7 +411,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru Context("deployment configuration", func() { It("should have deployment available with correct configuration", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) By("waiting for trust-manager deployment to become available") err := pollTillDeploymentAvailable(ctx, clientset, trustManagerNamespace, trustManagerDeploymentName) @@ -448,7 +432,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should update deployment args when log level changes", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) err := pollTillDeploymentAvailable(ctx, clientset, trustManagerNamespace, trustManagerDeploymentName) Expect(err).ShouldNot(HaveOccurred()) @@ -474,7 +458,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should apply custom resource requirements to deployment", func() { - createTrustManager(newTrustManagerCR().WithResources(corev1.ResourceRequirements{ + createTrustManager(ctx, newTrustManagerCR().WithResources(corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("50m"), corev1.ResourceMemory: resource.MustParse("64Mi"), @@ -499,7 +483,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should apply custom tolerations to deployment", func() { - createTrustManager(newTrustManagerCR().WithTolerations([]corev1.Toleration{ + createTrustManager(ctx, newTrustManagerCR().WithTolerations([]corev1.Toleration{ { Key: "test-key", Operator: corev1.TolerationOpEqual, @@ -525,7 +509,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should apply custom nodeSelector to deployment", func() { - createTrustManager(newTrustManagerCR().WithNodeSelector(map[string]string{ + createTrustManager(ctx, newTrustManagerCR().WithNodeSelector(map[string]string{ "test-node-label": "test-value", })) @@ -538,7 +522,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should apply custom affinity to deployment", func() { - createTrustManager(newTrustManagerCR().WithAffinity(&corev1.Affinity{ + createTrustManager(ctx, newTrustManagerCR().WithAffinity(&corev1.Affinity{ PodAntiAffinity: &corev1.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{ { @@ -574,7 +558,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru customTrustNS := createUniqueNamespace("custom-trust-ns") createAndDestroyTestNamespace(ctx, clientset, customTrustNS) - createTrustManager(newTrustManagerCR().WithTrustNamespace(customTrustNS)) + createTrustManager(ctx, newTrustManagerCR().WithTrustNamespace(customTrustNS)) By("verifying deployment has correct --trust-namespace arg") Eventually(func(g Gomega) { @@ -586,7 +570,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should add secret-targets-enabled arg when secretTargets policy is Custom", func() { - createTrustManager(newTrustManagerCR().WithSecretTargets(v1alpha1.SecretTargetsPolicyCustom, []string{"test-secret"})) + createTrustManager(ctx, newTrustManagerCR().WithSecretTargets(v1alpha1.SecretTargetsPolicyCustom, []string{"test-secret"})) By("verifying deployment args contain --secret-targets-enabled=true") Eventually(func(g Gomega) { @@ -598,7 +582,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should not have secret-targets-enabled arg when secretTargets is Disabled", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) By("verifying deployment args do not contain --secret-targets-enabled=true") Eventually(func(g Gomega) { @@ -609,8 +593,31 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }, lowTimeout, fastPollInterval).Should(Succeed()) }) - // TODO: Add test for other deployment configuration options - // (e.g. filter expired certificates policy; custom trust namespace is covered above.) + It("should add filter-expired-certificates arg when filterExpiredCertificates is Enabled", func() { + createTrustManager(ctx, newTrustManagerCR(). + WithFilterExpiredCertificates(v1alpha1.FilterExpiredCertificatesPolicyEnabled)) + + By("verifying deployment args contain --filter-expired-certificates=true") + Eventually(func(g Gomega) { + dep, err := clientset.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(dep.Spec.Template.Spec.Containers).ShouldNot(BeEmpty()) + g.Expect(dep.Spec.Template.Spec.Containers[0].Args).Should(ContainElement("--filter-expired-certificates=true")) + }, lowTimeout, fastPollInterval).Should(Succeed()) + }) + + It("should not have filter-expired-certificates arg when filterExpiredCertificates is Disabled", func() { + createTrustManager(ctx, newTrustManagerCR()) + + By("verifying deployment args do not contain --filter-expired-certificates=true") + Eventually(func(g Gomega) { + dep, err := clientset.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(dep.Spec.Template.Spec.Containers).ShouldNot(BeEmpty()) + g.Expect(dep.Spec.Template.Spec.Containers[0].Args).ShouldNot(ContainElement("--filter-expired-certificates=true")) + }, lowTimeout, fastPollInterval).Should(Succeed()) + }) + }) // ------------------------------------------------------------------------- @@ -619,7 +626,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru Context("default CA package configuration", func() { It("should reconcile deployment when default CA package policy transitions between Disabled and Enabled", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) By("verifying no default CA package ConfigMap exists") Eventually(func(g Gomega) { @@ -668,7 +675,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru return err }, lowTimeout, fastPollInterval).Should(Succeed()) - waitForTrustManagerReady() + waitForTrustManagerReady(ctx) By("verifying the CNO-injected CA bundle ConfigMap exists in operator namespace") Eventually(func(g Gomega) { @@ -750,7 +757,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru return err }, lowTimeout, fastPollInterval).Should(Succeed()) - waitForTrustManagerReady() + waitForTrustManagerReady(ctx) By("verifying deployment does not have --default-package-location arg after disabling") Eventually(func(g Gomega) { @@ -791,7 +798,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should reconcile ConfigMap data drift when CA package ConfigMap is tampered", func() { - createTrustManager(newTrustManagerCR().WithDefaultCAPackage(v1alpha1.DefaultCAPackagePolicyEnabled)) + createTrustManager(ctx, newTrustManagerCR().WithDefaultCAPackage(v1alpha1.DefaultCAPackagePolicyEnabled)) var originalData string By("reading original CA package ConfigMap data") @@ -817,6 +824,180 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru g.Expect(cm.Data["cert-manager-package-openshift.json"]).Should(Equal(originalData)) }, lowTimeout, fastPollInterval).Should(Succeed()) }) + + It("should propagate CNO CA bundle update to package ConfigMap and restart pod", func() { + const openshiftConfigNS = "openshift-config" + const userCABundleName = "user-ca-bundle" + + By("checking if cluster has external control plane (HyperShift) — Proxy is immutable there") + infra, err := configClient.Infrastructures().Get(ctx, "cluster", metav1.GetOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + if infra.Status.ControlPlaneTopology == configopenshiftv1.ExternalTopologyMode { + Skip("Proxy/cluster is immutable on HostedCluster (external control plane)") + } + + createTrustManager(ctx, newTrustManagerCR().WithDefaultCAPackage(v1alpha1.DefaultCAPackagePolicyEnabled)) + + // --- Capture baseline state --- + + var originalHash string + By("reading original hash annotation from pod template") + Eventually(func(g Gomega) { + dep, err := clientset.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(dep.Spec.Template.Annotations).Should(HaveKey(defaultCAPackageHashAnnotation)) + originalHash = dep.Spec.Template.Annotations[defaultCAPackageHashAnnotation] + g.Expect(originalHash).ShouldNot(BeEmpty()) + }, lowTimeout, fastPollInterval).Should(Succeed()) + + var originalPkgData string + By("reading original package ConfigMap data") + Eventually(func(g Gomega) { + cm, err := clientset.CoreV1().ConfigMaps(trustManagerNamespace).Get(ctx, defaultCAPackageConfigMapName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + originalPkgData = cm.Data["cert-manager-package-openshift.json"] + g.Expect(originalPkgData).ShouldNot(BeEmpty()) + }, lowTimeout, fastPollInterval).Should(Succeed()) + + var originalInjectedData string + By("reading original CNO-injected CA bundle ConfigMap data") + Eventually(func(g Gomega) { + cm, err := clientset.CoreV1().ConfigMaps(operatorNamespace).Get(ctx, trustedCABundleConfigMapName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + originalInjectedData = cm.Data[trustedCABundleKey] + }, lowTimeout, fastPollInterval).Should(Succeed()) + + // --- Save original Proxy and user-ca-bundle state for cleanup --- + + By("saving original Proxy trustedCA and user-ca-bundle state") + proxy, err := configClient.Proxies().Get(ctx, "cluster", metav1.GetOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + originalTrustedCAName := proxy.Spec.TrustedCA.Name + + existingUserCA, userCAErr := clientset.CoreV1().ConfigMaps(openshiftConfigNS).Get(ctx, userCABundleName, metav1.GetOptions{}) + userCABundleExisted := userCAErr == nil + var originalUserCAData string + if userCABundleExisted { + originalUserCAData = existingUserCA.Data["ca-bundle.crt"] + } + + // --- Register cleanup to restore original cluster state --- + + DeferCleanup(func() { + By("[cleanup] restoring original Proxy trustedCA reference") + if originalTrustedCAName != userCABundleName { + Eventually(func() error { + p, err := configClient.Proxies().Get(ctx, "cluster", metav1.GetOptions{}) + if err != nil { + return err + } + p.Spec.TrustedCA.Name = originalTrustedCAName + _, err = configClient.Proxies().Update(ctx, p, metav1.UpdateOptions{}) + return err + }, lowTimeout, fastPollInterval).Should(Succeed()) + } + + By("[cleanup] restoring original user-ca-bundle ConfigMap") + if userCABundleExisted { + Eventually(func() error { + cm, err := clientset.CoreV1().ConfigMaps(openshiftConfigNS).Get(ctx, userCABundleName, metav1.GetOptions{}) + if err != nil { + return err + } + cm.Data["ca-bundle.crt"] = originalUserCAData + _, err = clientset.CoreV1().ConfigMaps(openshiftConfigNS).Update(ctx, cm, metav1.UpdateOptions{}) + return err + }, lowTimeout, fastPollInterval).Should(Succeed()) + } else { + _ = clientset.CoreV1().ConfigMaps(openshiftConfigNS).Delete(ctx, userCABundleName, metav1.DeleteOptions{}) + } + + By("[cleanup] waiting for CNO to restore the injected CA bundle") + Eventually(func(g Gomega) { + cm, err := clientset.CoreV1().ConfigMaps(operatorNamespace).Get(ctx, trustedCABundleConfigMapName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(cm.Data[trustedCABundleKey]).Should(Equal(originalInjectedData), + "injected CA bundle should be restored to original state") + }, highTimeout, slowPollInterval).Should(Succeed()) + }) + + // --- Modify the cluster-wide trust bundle via Proxy CR --- + + testCACert := testutils.GenerateCertificate( + "e2e-proxy-test-ca", + []string{"cert-manager-operator-e2e"}, + func(cert *x509.Certificate) { + cert.IsCA = true + cert.KeyUsage |= x509.KeyUsageCertSign + }, + ) + + By("creating/updating user-ca-bundle ConfigMap in openshift-config namespace") + if userCABundleExisted { + Eventually(func() error { + cm, err := clientset.CoreV1().ConfigMaps(openshiftConfigNS).Get(ctx, userCABundleName, metav1.GetOptions{}) + if err != nil { + return err + } + cm.Data["ca-bundle.crt"] = cm.Data["ca-bundle.crt"] + "\n" + testCACert + _, err = clientset.CoreV1().ConfigMaps(openshiftConfigNS).Update(ctx, cm, metav1.UpdateOptions{}) + return err + }, lowTimeout, fastPollInterval).Should(Succeed()) + } else { + _, err := clientset.CoreV1().ConfigMaps(openshiftConfigNS).Create(ctx, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: userCABundleName, + Namespace: openshiftConfigNS, + }, + Data: map[string]string{ + "ca-bundle.crt": testCACert, + }, + }, metav1.CreateOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + } + + if originalTrustedCAName != userCABundleName { + By("updating Proxy/cluster to reference user-ca-bundle") + Eventually(func() error { + p, err := configClient.Proxies().Get(ctx, "cluster", metav1.GetOptions{}) + if err != nil { + return err + } + p.Spec.TrustedCA.Name = userCABundleName + _, err = configClient.Proxies().Update(ctx, p, metav1.UpdateOptions{}) + return err + }, lowTimeout, fastPollInterval).Should(Succeed()) + } + + // --- Wait for CNO propagation --- + + By("waiting for CNO to propagate updated CA bundle to operator namespace") + Eventually(func(g Gomega) { + cm, err := clientset.CoreV1().ConfigMaps(operatorNamespace).Get(ctx, trustedCABundleConfigMapName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(cm.Data[trustedCABundleKey]).ShouldNot(Equal(originalInjectedData), + "CNO should have updated the injected CA bundle") + }, highTimeout, slowPollInterval).Should(Succeed()) + + // --- Verify operator propagated the change --- + + By("verifying package ConfigMap in operand namespace is updated with new content") + Eventually(func(g Gomega) { + cm, err := clientset.CoreV1().ConfigMaps(trustManagerNamespace).Get(ctx, defaultCAPackageConfigMapName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(cm.Data["cert-manager-package-openshift.json"]).ShouldNot(Equal(originalPkgData), + "package ConfigMap should reflect the updated CA bundle") + }, highTimeout, fastPollInterval).Should(Succeed()) + + By("verifying pod template hash annotation changed (triggers rolling restart)") + Eventually(func(g Gomega) { + dep, err := clientset.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(dep.Spec.Template.Annotations).Should(HaveKey(defaultCAPackageHashAnnotation)) + g.Expect(dep.Spec.Template.Annotations[defaultCAPackageHashAnnotation]).ShouldNot(Equal(originalHash), + "hash annotation should have changed after CA bundle update") + }, highTimeout, fastPollInterval).Should(Succeed()) + }) }) // ------------------------------------------------------------------------- @@ -825,7 +1006,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru Context("RBAC configuration", func() { It("should configure ClusterRoleBinding with correct subjects and roleRef", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) By("verifying ClusterRoleBinding references correct ClusterRole and ServiceAccount") Eventually(func(g Gomega) { @@ -842,7 +1023,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should configure trust namespace RoleBinding with correct subjects and roleRef", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) By("verifying trust namespace RoleBinding references correct Role and ServiceAccount") Eventually(func(g Gomega) { @@ -859,7 +1040,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should configure leader election RoleBinding with correct subjects and roleRef", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) By("verifying leader election RoleBinding references correct Role and ServiceAccount") Eventually(func(g Gomega) { @@ -881,7 +1062,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru createAndDestroyTestNamespace(ctx, clientset, customTrustNS) By("creating TrustManager CR with custom trust namespace") - createTrustManager(newTrustManagerCR().WithTrustNamespace(customTrustNS)) + createTrustManager(ctx, newTrustManagerCR().WithTrustNamespace(customTrustNS)) By("verifying Role is created in custom trust namespace") Eventually(func(g Gomega) { @@ -910,7 +1091,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru createAndDestroyTestNamespace(ctx, clientset, customTrustNS) By("creating TrustManager CR with custom trust namespace") - createTrustManager(newTrustManagerCR().WithTrustNamespace(customTrustNS)) + createTrustManager(ctx, newTrustManagerCR().WithTrustNamespace(customTrustNS)) By("verifying leader election Role is in operand namespace") Eventually(func(g Gomega) { @@ -930,7 +1111,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should have no secret rules on ClusterRole when secretTargets is Disabled", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) By("verifying ClusterRole has no secret rules") Eventually(func(g Gomega) { @@ -942,7 +1123,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru It("should add secret read and scoped write rules to ClusterRole when secretTargets is Custom", func() { authorizedSecrets := []string{"bundle-secret-a", "bundle-secret-b"} - createTrustManager(newTrustManagerCR().WithSecretTargets(v1alpha1.SecretTargetsPolicyCustom, authorizedSecrets)) + createTrustManager(ctx, newTrustManagerCR().WithSecretTargets(v1alpha1.SecretTargetsPolicyCustom, authorizedSecrets)) By("verifying ClusterRole has secret read rule") Eventually(func(g Gomega) { @@ -967,8 +1148,10 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }, lowTimeout, fastPollInterval).Should(Succeed()) }) - It("should update ClusterRole rules when secretTargets policy changes from Disabled to Custom", func() { - createTrustManager(newTrustManagerCR()) + It("should update ClusterRole rules when secretTargets policy transitions between Disabled and Custom", func() { + createTrustManager(ctx, newTrustManagerCR()) + + // --- Disabled → Custom --- By("verifying ClusterRole initially has no secret rules") Eventually(func(g Gomega) { @@ -1000,10 +1183,97 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru g.Expect(writeRule).ShouldNot(BeNil(), "expected secret write rule after enabling secretTargets") g.Expect(writeRule.ResourceNames).Should(ConsistOf("updated-secret")) }, lowTimeout, fastPollInterval).Should(Succeed()) + + // --- Custom → Disabled --- + + By("updating TrustManager CR to disable secretTargets") + Eventually(func() error { + tm, err := trustManagerClient().Get(ctx, "cluster", metav1.GetOptions{}) + if err != nil { + return err + } + tm.Spec.TrustManagerConfig.SecretTargets = v1alpha1.SecretTargetsConfig{ + Policy: v1alpha1.SecretTargetsPolicyDisabled, + } + _, err = trustManagerClient().Update(ctx, tm, metav1.UpdateOptions{}) + return err + }, lowTimeout, fastPollInterval).Should(Succeed()) + + waitForTrustManagerReady(ctx) + + By("verifying ClusterRole no longer has secret rules after disabling") + Eventually(func(g Gomega) { + cr, err := clientset.RbacV1().ClusterRoles().Get(ctx, trustManagerClusterRoleName, metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(hasSecretRule(cr.Rules)).Should(BeFalse(), + "ClusterRole should not have secret rules after disabling secretTargets") + }, lowTimeout, fastPollInterval).Should(Succeed()) + }) + + It("should reject Custom policy with empty authorizedSecrets on create", func() { + _, err := trustManagerClient().Create(ctx, &v1alpha1.TrustManager{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster"}, + Spec: v1alpha1.TrustManagerSpec{ + TrustManagerConfig: v1alpha1.TrustManagerConfig{ + SecretTargets: v1alpha1.SecretTargetsConfig{ + Policy: v1alpha1.SecretTargetsPolicyCustom, + }, + }, + }, + }, metav1.CreateOptions{}) + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("authorizedSecrets must not be empty when policy is Custom")) + }) + + It("should reject Disabled policy with authorizedSecrets on create", func() { + _, err := trustManagerClient().Create(ctx, &v1alpha1.TrustManager{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster"}, + Spec: v1alpha1.TrustManagerSpec{ + TrustManagerConfig: v1alpha1.TrustManagerConfig{ + SecretTargets: v1alpha1.SecretTargetsConfig{ + Policy: v1alpha1.SecretTargetsPolicyDisabled, + AuthorizedSecrets: []string{"my-secret"}, + }, + }, + }, + }, metav1.CreateOptions{}) + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("authorizedSecrets must be empty when policy is not Custom")) + }) + + It("should reject emptying authorizedSecrets while policy remains Custom", func() { + createTrustManager(ctx, newTrustManagerCR(). + WithSecretTargets(v1alpha1.SecretTargetsPolicyCustom, []string{"my-secret"})) + + By("attempting to set authorizedSecrets to empty while keeping Custom policy") + tm, err := trustManagerClient().Get(ctx, "cluster", metav1.GetOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + tm.Spec.TrustManagerConfig.SecretTargets = v1alpha1.SecretTargetsConfig{ + Policy: v1alpha1.SecretTargetsPolicyCustom, + AuthorizedSecrets: []string{}, + } + _, err = trustManagerClient().Update(ctx, tm, metav1.UpdateOptions{}) + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("authorizedSecrets must not be empty when policy is Custom")) + }) + + It("should reject adding authorizedSecrets while policy remains Disabled", func() { + createTrustManager(ctx, newTrustManagerCR()) + + By("attempting to add authorizedSecrets while keeping Disabled policy") + tm, err := trustManagerClient().Get(ctx, "cluster", metav1.GetOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + tm.Spec.TrustManagerConfig.SecretTargets = v1alpha1.SecretTargetsConfig{ + Policy: v1alpha1.SecretTargetsPolicyDisabled, + AuthorizedSecrets: []string{"my-secret"}, + } + _, err = trustManagerClient().Update(ctx, tm, metav1.UpdateOptions{}) + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("authorizedSecrets must be empty when policy is not Custom")) }) It("should update ClusterRole resourceNames when authorizedSecrets list changes", func() { - createTrustManager(newTrustManagerCR(). + createTrustManager(ctx, newTrustManagerCR(). WithSecretTargets(v1alpha1.SecretTargetsPolicyCustom, []string{"secret-a", "secret-b"})) By("verifying ClusterRole has initial authorized secrets") @@ -1048,7 +1318,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru Context("webhook and certificate configuration", func() { It("should configure webhook with cert-manager CA injection annotation", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) expectedAnnotation := fmt.Sprintf("%s/%s", trustManagerNamespace, trustManagerCertificateName) @@ -1061,7 +1331,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should configure webhook service reference correctly", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) By("verifying webhook service references are correct") Eventually(func(g Gomega) { @@ -1078,7 +1348,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should have Issuer become ready", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) By("waiting for trust-manager Issuer to become ready") err := waitForIssuerReadiness(ctx, trustManagerIssuerName, trustManagerNamespace) @@ -1086,7 +1356,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should have Certificate become ready and create TLS secret", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) By("waiting for trust-manager Certificate to become ready") err := waitForCertificateReadiness(ctx, trustManagerCertificateName, trustManagerNamespace) @@ -1103,7 +1373,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should configure Certificate with correct spec fields", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) expectedDNSName := fmt.Sprintf("%s.%s.svc", trustManagerServiceName, trustManagerNamespace) @@ -1126,7 +1396,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru Context("status reporting", func() { It("should report trust-manager image in status", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) By("verifying TrustManager status has image set") Eventually(func(g Gomega) { @@ -1137,7 +1407,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should report trust namespace in status", func() { - createTrustManager(newTrustManagerCR()) + createTrustManager(ctx, newTrustManagerCR()) By("verifying TrustManager status has default trust namespace set") Eventually(func(g Gomega) { @@ -1152,7 +1422,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru customTrustNS := createUniqueNamespace("custom-trust-ns-status") createAndDestroyTestNamespace(ctx, clientset, customTrustNS) - createTrustManager(newTrustManagerCR().WithTrustNamespace(customTrustNS)) + createTrustManager(ctx, newTrustManagerCR().WithTrustNamespace(customTrustNS)) By("verifying TrustManager status has custom trust namespace set") Eventually(func(g Gomega) { @@ -1163,7 +1433,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should report secretTargets policy in status", func() { - createTrustManager(newTrustManagerCR().WithSecretTargets(v1alpha1.SecretTargetsPolicyCustom, []string{"status-test-secret"})) + createTrustManager(ctx, newTrustManagerCR().WithSecretTargets(v1alpha1.SecretTargetsPolicyCustom, []string{"status-test-secret"})) By("verifying TrustManager status reflects Custom secretTargets policy") Eventually(func(g Gomega) { @@ -1174,7 +1444,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }) It("should report default CA package policy in status", func() { - createTrustManager(newTrustManagerCR().WithDefaultCAPackage(v1alpha1.DefaultCAPackagePolicyEnabled)) + createTrustManager(ctx, newTrustManagerCR().WithDefaultCAPackage(v1alpha1.DefaultCAPackagePolicyEnabled)) By("verifying status reports Enabled policy") Eventually(func(g Gomega) { @@ -1202,8 +1472,17 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru }, lowTimeout, fastPollInterval).Should(Succeed()) }) - // TODO: Add test for status reporting when custom configuration is applied - // (e.g. filter expired certificates policy; secret targets, default CA package, and trust namespace are covered above.) + It("should report filterExpiredCertificates policy in status", func() { + createTrustManager(ctx, newTrustManagerCR(). + WithFilterExpiredCertificates(v1alpha1.FilterExpiredCertificatesPolicyEnabled)) + + By("verifying status reports Enabled policy") + Eventually(func(g Gomega) { + tm, err := trustManagerClient().Get(ctx, "cluster", metav1.GetOptions{}) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(tm.Status.FilterExpiredCertificatesPolicy).Should(Equal(v1alpha1.FilterExpiredCertificatesPolicyEnabled)) + }, lowTimeout, fastPollInterval).Should(Succeed()) + }) }) // ------------------------------------------------------------------------- @@ -1213,7 +1492,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru Context("trust namespace configuration", func() { It("should reject updates that change spec.trustNamespace", func() { By("creating TrustManager with explicit trust namespace") - createTrustManager(newTrustManagerCR().WithTrustNamespace("cert-manager")) + createTrustManager(ctx, newTrustManagerCR().WithTrustNamespace("cert-manager")) By("attempting to mutate spec.trustNamespace (field is immutable once set)") tm, err := trustManagerClient().Get(ctx, "cluster", metav1.GetOptions{}) @@ -1273,7 +1552,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru Expect(err).ShouldNot(HaveOccurred()) By("waiting for TrustManager to recover (Ready=True, not Degraded)") - waitForTrustManagerReady() + waitForTrustManagerReady(ctx) }) }) @@ -1284,7 +1563,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru Context("custom labels and annotations", func() { It("should apply custom labels from controllerConfig to all managed resources", func() { By("creating TrustManager CR with custom labels") - createTrustManager(newTrustManagerCR().WithLabels(map[string]string{ + createTrustManager(ctx, newTrustManagerCR().WithLabels(map[string]string{ "custom-label": "custom-value", })) @@ -1340,7 +1619,7 @@ var _ = Describe("TrustManager", Ordered, Label("Platform:Generic", "Feature:Tru It("should apply custom annotations from controllerConfig to managed resources", func() { By("creating TrustManager CR with custom annotations") - createTrustManager(newTrustManagerCR().WithAnnotations(map[string]string{ + createTrustManager(ctx, newTrustManagerCR().WithAnnotations(map[string]string{ "custom-annotation": "annotation-value", })) diff --git a/test/e2e/utils_test.go b/test/e2e/utils_test.go index d59cc5fbb..1170ca1fd 100644 --- a/test/e2e/utils_test.go +++ b/test/e2e/utils_test.go @@ -27,7 +27,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" . "github.com/onsi/ginkgo/v2" - "github.com/onsi/gomega" + . "github.com/onsi/gomega" opv1 "github.com/openshift/api/operator/v1" configv1 "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1" operatorv1 "github.com/openshift/client-go/operator/clientset/versioned/typed/operator/v1" @@ -103,73 +103,6 @@ var serviceMonitorGVR = schema.GroupVersionResource{ Resource: "servicemonitors", } -type trustManagerCRBuilder struct { - tm *v1alpha1.TrustManager -} - -func newTrustManagerCR() *trustManagerCRBuilder { - return &trustManagerCRBuilder{ - tm: &v1alpha1.TrustManager{ - ObjectMeta: metav1.ObjectMeta{Name: "cluster"}, - Spec: v1alpha1.TrustManagerSpec{ - TrustManagerConfig: v1alpha1.TrustManagerConfig{}, - }, - }, - } -} - -func (b *trustManagerCRBuilder) WithResources(resources corev1.ResourceRequirements) *trustManagerCRBuilder { - b.tm.Spec.TrustManagerConfig.Resources = resources - return b -} - -func (b *trustManagerCRBuilder) WithTolerations(tolerations []corev1.Toleration) *trustManagerCRBuilder { - b.tm.Spec.TrustManagerConfig.Tolerations = tolerations - return b -} - -func (b *trustManagerCRBuilder) WithNodeSelector(nodeSelector map[string]string) *trustManagerCRBuilder { - b.tm.Spec.TrustManagerConfig.NodeSelector = nodeSelector - return b -} - -func (b *trustManagerCRBuilder) WithAffinity(affinity *corev1.Affinity) *trustManagerCRBuilder { - b.tm.Spec.TrustManagerConfig.Affinity = affinity - return b -} - -func (b *trustManagerCRBuilder) WithLabels(labels map[string]string) *trustManagerCRBuilder { - b.tm.Spec.ControllerConfig.Labels = labels - return b -} - -func (b *trustManagerCRBuilder) WithAnnotations(annotations map[string]string) *trustManagerCRBuilder { - b.tm.Spec.ControllerConfig.Annotations = annotations - return b -} - -func (b *trustManagerCRBuilder) WithTrustNamespace(trustNamespace string) *trustManagerCRBuilder { - b.tm.Spec.TrustManagerConfig.TrustNamespace = trustNamespace - return b -} - -func (b *trustManagerCRBuilder) WithSecretTargets(policy v1alpha1.SecretTargetsPolicy, authorizedSecrets []string) *trustManagerCRBuilder { - b.tm.Spec.TrustManagerConfig.SecretTargets = v1alpha1.SecretTargetsConfig{ - Policy: policy, - AuthorizedSecrets: authorizedSecrets, - } - return b -} - -func (b *trustManagerCRBuilder) WithDefaultCAPackage(policy v1alpha1.DefaultCAPackagePolicy) *trustManagerCRBuilder { - b.tm.Spec.TrustManagerConfig.DefaultCAPackage.Policy = policy - return b -} - -func (b *trustManagerCRBuilder) Build() *v1alpha1.TrustManager { - return b.tm -} - func verifyDeploymentGenerationIsNotEmpty(client *certmanoperatorclient.Clientset, deployments []metav1.ObjectMeta) error { var wg sync.WaitGroup var lastFetchedGenerationStatus []opv1.GenerationStatus @@ -1090,7 +1023,7 @@ func verifyCertificateRenewed(ctx context.Context, secretName, namespace string, func createUniqueNamespace(prefix string) string { var b [4]byte _, err := cryptorand.Read(b[:]) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) + Expect(err).NotTo(HaveOccurred()) name := fmt.Sprintf("%s-%s", prefix, hex.EncodeToString(b[:])) if len(name) > 63 { return name[:63] @@ -1100,10 +1033,10 @@ func createUniqueNamespace(prefix string) string { // waitNamespaceDeleted polls until the namespace is gone. func waitNamespaceDeleted(ctx context.Context, clientset *kubernetes.Clientset, name string) { - gomega.Eventually(func() bool { + Eventually(func() bool { _, err := clientset.CoreV1().Namespaces().Get(ctx, name, metav1.GetOptions{}) return apierrors.IsNotFound(err) - }, lowTimeout, fastPollInterval).Should(gomega.BeTrue()) + }, lowTimeout, fastPollInterval).Should(BeTrue()) } // createAndDestroyTestNamespace creates a namespace with the given name and registers DeferCleanup @@ -1112,7 +1045,7 @@ func createAndDestroyTestNamespace(ctx context.Context, clientset *kubernetes.Cl _, err := clientset.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: name}, }, metav1.CreateOptions{}) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) + Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { By("cleaning up test namespace") _ = clientset.CoreV1().Namespaces().Delete(ctx, name, metav1.DeleteOptions{}) diff --git a/test/go.mod b/test/go.mod index f97ab0d91..6e45dcb29 100644 --- a/test/go.mod +++ b/test/go.mod @@ -150,7 +150,7 @@ require ( sigs.k8s.io/gateway-api v1.4.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) replace github.com/openshift/cert-manager-operator => ../ diff --git a/test/go.sum b/test/go.sum index d2881a84e..ce3d1e60b 100644 --- a/test/go.sum +++ b/test/go.sum @@ -428,7 +428,7 @@ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5E sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/tools/go.mod b/tools/go.mod index 72c485107..21757b4b7 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -83,7 +83,7 @@ require ( github.com/fzipp/gocyclo v0.6.0 // indirect github.com/ghostiam/protogetter v0.3.17 // indirect github.com/go-critic/go-critic v0.14.2 // indirect - github.com/go-errors/errors v1.4.2 // indirect + github.com/go-errors/errors v1.5.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect @@ -292,7 +292,7 @@ require ( sigs.k8s.io/kustomize/cmd/config v0.20.1 // indirect sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/tools/go.sum b/tools/go.sum index 6a7065a63..73f4bfca2 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -150,8 +150,8 @@ github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7Y github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-critic/go-critic v0.14.2 h1:PMvP5f+LdR8p6B29npvChUXbD1vrNlKDf60NJtgMBOo= github.com/go-critic/go-critic v0.14.2/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= -github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -814,7 +814,7 @@ sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/vendor/github.com/go-errors/errors/.travis.yml b/vendor/github.com/go-errors/errors/.travis.yml index 77a6bccf7..1dc296026 100644 --- a/vendor/github.com/go-errors/errors/.travis.yml +++ b/vendor/github.com/go-errors/errors/.travis.yml @@ -2,7 +2,6 @@ language: go go: - "1.8.x" - - "1.10.x" - - "1.13.x" - - "1.14.x" + - "1.11.x" - "1.16.x" + - "1.21.x" diff --git a/vendor/github.com/go-errors/errors/README.md b/vendor/github.com/go-errors/errors/README.md index 3d7852594..558bc883e 100644 --- a/vendor/github.com/go-errors/errors/README.md +++ b/vendor/github.com/go-errors/errors/README.md @@ -9,7 +9,7 @@ This is particularly useful when you want to understand the state of execution when an error was returned unexpectedly. It provides the type \*Error which implements the standard golang error -interface, so you can use this library interchangably with code that is +interface, so you can use this library interchangeably with code that is expecting a normal error return. Usage @@ -80,3 +80,5 @@ This package is licensed under the MIT license, see LICENSE.MIT for details. * v1.4.0 *BREAKING* v1.4.0 reverted all changes from v1.3.0 and is identical to v1.2.0 * v1.4.1 no code change, but now without an unnecessary cover.out file. * v1.4.2 performance improvement to ErrorStack() to avoid unnecessary work https://github.com/go-errors/errors/pull/40 +* v1.5.0 add errors.Join() and errors.Unwrap() copying the stdlib https://github.com/go-errors/errors/pull/40 +* v1.5.1 fix build on go1.13..go1.19 (broken by adding Join and Unwrap with wrong build constraints) diff --git a/vendor/github.com/go-errors/errors/error_1_13.go b/vendor/github.com/go-errors/errors/error_1_13.go index 0af2fc806..34ab3e00e 100644 --- a/vendor/github.com/go-errors/errors/error_1_13.go +++ b/vendor/github.com/go-errors/errors/error_1_13.go @@ -1,3 +1,4 @@ +//go:build go1.13 // +build go1.13 package errors @@ -6,14 +7,17 @@ import ( baseErrors "errors" ) -// find error in any wrapped error +// As finds the first error in err's tree that matches target, and if one is found, sets +// target to that error value and returns true. Otherwise, it returns false. +// +// For more information see stdlib errors.As. func As(err error, target interface{}) bool { return baseErrors.As(err, target) } // Is detects whether the error is equal to a given error. Errors // are considered equal by this function if they are matched by errors.Is -// or if their contained errors are matched through errors.Is +// or if their contained errors are matched through errors.Is. func Is(e error, original error) bool { if baseErrors.Is(e, original) { return true diff --git a/vendor/github.com/go-errors/errors/error_backward.go b/vendor/github.com/go-errors/errors/error_backward.go index 80b0695e7..ff14c4bfa 100644 --- a/vendor/github.com/go-errors/errors/error_backward.go +++ b/vendor/github.com/go-errors/errors/error_backward.go @@ -1,3 +1,4 @@ +//go:build !go1.13 // +build !go1.13 package errors @@ -55,3 +56,70 @@ func Is(e error, original error) bool { return false } + +// Disclaimer: functions Join and Unwrap are copied from the stdlib errors +// package v1.21.0. + +// Join returns an error that wraps the given errors. +// Any nil error values are discarded. +// Join returns nil if every value in errs is nil. +// The error formats as the concatenation of the strings obtained +// by calling the Error method of each element of errs, with a newline +// between each string. +// +// A non-nil error returned by Join implements the Unwrap() []error method. +func Join(errs ...error) error { + n := 0 + for _, err := range errs { + if err != nil { + n++ + } + } + if n == 0 { + return nil + } + e := &joinError{ + errs: make([]error, 0, n), + } + for _, err := range errs { + if err != nil { + e.errs = append(e.errs, err) + } + } + return e +} + +type joinError struct { + errs []error +} + +func (e *joinError) Error() string { + var b []byte + for i, err := range e.errs { + if i > 0 { + b = append(b, '\n') + } + b = append(b, err.Error()...) + } + return string(b) +} + +func (e *joinError) Unwrap() []error { + return e.errs +} + +// Unwrap returns the result of calling the Unwrap method on err, if err's +// type contains an Unwrap method returning error. +// Otherwise, Unwrap returns nil. +// +// Unwrap only calls a method of the form "Unwrap() error". +// In particular Unwrap does not unwrap errors returned by [Join]. +func Unwrap(err error) error { + u, ok := err.(interface { + Unwrap() error + }) + if !ok { + return nil + } + return u.Unwrap() +} diff --git a/vendor/github.com/go-errors/errors/join_unwrap_1_20.go b/vendor/github.com/go-errors/errors/join_unwrap_1_20.go new file mode 100644 index 000000000..44df35ece --- /dev/null +++ b/vendor/github.com/go-errors/errors/join_unwrap_1_20.go @@ -0,0 +1,32 @@ +//go:build go1.20 +// +build go1.20 + +package errors + +import baseErrors "errors" + +// Join returns an error that wraps the given errors. +// Any nil error values are discarded. +// Join returns nil if every value in errs is nil. +// The error formats as the concatenation of the strings obtained +// by calling the Error method of each element of errs, with a newline +// between each string. +// +// A non-nil error returned by Join implements the Unwrap() []error method. +// +// For more information see stdlib errors.Join. +func Join(errs ...error) error { + return baseErrors.Join(errs...) +} + +// Unwrap returns the result of calling the Unwrap method on err, if err's +// type contains an Unwrap method returning error. +// Otherwise, Unwrap returns nil. +// +// Unwrap only calls a method of the form "Unwrap() error". +// In particular Unwrap does not unwrap errors returned by [Join]. +// +// For more information see stdlib errors.Unwrap. +func Unwrap(err error) error { + return baseErrors.Unwrap(err) +} diff --git a/vendor/github.com/go-errors/errors/join_unwrap_backward.go b/vendor/github.com/go-errors/errors/join_unwrap_backward.go new file mode 100644 index 000000000..50c766976 --- /dev/null +++ b/vendor/github.com/go-errors/errors/join_unwrap_backward.go @@ -0,0 +1,71 @@ +//go:build !go1.20 +// +build !go1.20 + +package errors + +// Disclaimer: functions Join and Unwrap are copied from the stdlib errors +// package v1.21.0. + +// Join returns an error that wraps the given errors. +// Any nil error values are discarded. +// Join returns nil if every value in errs is nil. +// The error formats as the concatenation of the strings obtained +// by calling the Error method of each element of errs, with a newline +// between each string. +// +// A non-nil error returned by Join implements the Unwrap() []error method. +func Join(errs ...error) error { + n := 0 + for _, err := range errs { + if err != nil { + n++ + } + } + if n == 0 { + return nil + } + e := &joinError{ + errs: make([]error, 0, n), + } + for _, err := range errs { + if err != nil { + e.errs = append(e.errs, err) + } + } + return e +} + +type joinError struct { + errs []error +} + +func (e *joinError) Error() string { + var b []byte + for i, err := range e.errs { + if i > 0 { + b = append(b, '\n') + } + b = append(b, err.Error()...) + } + return string(b) +} + +func (e *joinError) Unwrap() []error { + return e.errs +} + +// Unwrap returns the result of calling the Unwrap method on err, if err's +// type contains an Unwrap method returning error. +// Otherwise, Unwrap returns nil. +// +// Unwrap only calls a method of the form "Unwrap() error". +// In particular Unwrap does not unwrap errors returned by [Join]. +func Unwrap(err error) error { + u, ok := err.(interface { + Unwrap() error + }) + if !ok { + return nil + } + return u.Unwrap() +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 5073ad921..28b5d9db5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -490,7 +490,7 @@ github.com/go-critic/go-critic/checkers/internal/astwalk github.com/go-critic/go-critic/checkers/internal/lintutil github.com/go-critic/go-critic/checkers/rulesdata github.com/go-critic/go-critic/linter -# github.com/go-errors/errors v1.4.2 +# github.com/go-errors/errors v1.5.1 ## explicit; go 1.14 github.com/go-errors/errors # github.com/go-logr/logr v1.4.3 @@ -3380,7 +3380,7 @@ sigs.k8s.io/kustomize/kyaml/yaml/walk ## explicit; go 1.18 sigs.k8s.io/randfill sigs.k8s.io/randfill/bytesource -# sigs.k8s.io/structured-merge-diff/v6 v6.3.0 +# sigs.k8s.io/structured-merge-diff/v6 v6.3.1 ## explicit; go 1.23 sigs.k8s.io/structured-merge-diff/v6/fieldpath sigs.k8s.io/structured-merge-diff/v6/merge diff --git a/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/elements.go b/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/elements.go index 5d3707a5b..c8138a654 100644 --- a/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/elements.go +++ b/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/elements.go @@ -18,6 +18,7 @@ package schema import ( "sync" + "sync/atomic" ) // Schema is a list of named types. @@ -28,7 +29,7 @@ type Schema struct { Types []TypeDef `yaml:"types,omitempty"` once sync.Once - m map[string]TypeDef + m atomic.Pointer[map[string]TypeDef] lock sync.Mutex // Cached results of resolving type references to atoms. Only stores @@ -144,26 +145,28 @@ type Map struct { ElementRelationship ElementRelationship `yaml:"elementRelationship,omitempty"` once sync.Once - m map[string]StructField + m atomic.Pointer[map[string]StructField] } // FindField is a convenience function that returns the referenced StructField, // if it exists, or (nil, false) if it doesn't. func (m *Map) FindField(name string) (StructField, bool) { m.once.Do(func() { - m.m = make(map[string]StructField, len(m.Fields)) + mm := make(map[string]StructField, len(m.Fields)) for _, field := range m.Fields { - m.m[field.Name] = field + mm[field.Name] = field } + m.m.Store(&mm) }) - sf, ok := m.m[name] + sf, ok := (*m.m.Load())[name] return sf, ok } -// CopyInto this instance of Map into the other -// If other is nil this method does nothing. -// If other is already initialized, overwrites it with this instance -// Warning: Not thread safe +// CopyInto clones this instance of Map into dst +// +// If dst is nil this method does nothing. +// If dst is already initialized, overwrites it with this instance. +// Warning: Not thread safe. Only use dst after this function returns. func (m *Map) CopyInto(dst *Map) { if dst == nil { return @@ -175,12 +178,13 @@ func (m *Map) CopyInto(dst *Map) { dst.Unions = m.Unions dst.ElementRelationship = m.ElementRelationship - if m.m != nil { + mm := m.m.Load() + if mm != nil { // If cache is non-nil then the once token had been consumed. // Must reset token and use it again to ensure same semantics. dst.once = sync.Once{} dst.once.Do(func() { - dst.m = m.m + dst.m.Store(mm) }) } } @@ -274,12 +278,13 @@ type List struct { // if it exists, or (nil, false) if it doesn't. func (s *Schema) FindNamedType(name string) (TypeDef, bool) { s.once.Do(func() { - s.m = make(map[string]TypeDef, len(s.Types)) + sm := make(map[string]TypeDef, len(s.Types)) for _, t := range s.Types { - s.m[t.Name] = t + sm[t.Name] = t } + s.m.Store(&sm) }) - t, ok := s.m[name] + t, ok := (*s.m.Load())[name] return t, ok } @@ -352,10 +357,11 @@ func (s *Schema) Resolve(tr TypeRef) (Atom, bool) { return result, true } -// Clones this instance of Schema into the other -// If other is nil this method does nothing. -// If other is already initialized, overwrites it with this instance -// Warning: Not thread safe +// CopyInto clones this instance of Schema into dst +// +// If dst is nil this method does nothing. +// If dst is already initialized, overwrites it with this instance. +// Warning: Not thread safe. Only use dst after this function returns. func (s *Schema) CopyInto(dst *Schema) { if dst == nil { return @@ -364,12 +370,13 @@ func (s *Schema) CopyInto(dst *Schema) { // Schema type is considered immutable so sharing references dst.Types = s.Types - if s.m != nil { + sm := s.m.Load() + if sm != nil { // If cache is non-nil then the once token had been consumed. // Must reset token and use it again to ensure same semantics. dst.once = sync.Once{} dst.once.Do(func() { - dst.m = s.m + dst.m.Store(sm) }) } } diff --git a/vendor/sigs.k8s.io/structured-merge-diff/v6/typed/remove.go b/vendor/sigs.k8s.io/structured-merge-diff/v6/typed/remove.go index 86de5105d..0db1734f9 100644 --- a/vendor/sigs.k8s.io/structured-merge-diff/v6/typed/remove.go +++ b/vendor/sigs.k8s.io/structured-merge-diff/v6/typed/remove.go @@ -58,6 +58,10 @@ func (w *removingWalker) doList(t *schema.List) (errs ValidationErrors) { defer w.allocator.Free(l) // If list is null or empty just return if l == nil || l.Length() == 0 { + // For extraction, we just return the value as is (which is nil or empty). For extraction the difference matters. + if w.shouldExtract { + w.out = w.value.Unstructured() + } return nil } @@ -71,6 +75,7 @@ func (w *removingWalker) doList(t *schema.List) (errs ValidationErrors) { } var newItems []interface{} + hadMatches := false iter := l.RangeUsing(w.allocator) defer w.allocator.Free(iter) for iter.Next() { @@ -80,24 +85,40 @@ func (w *removingWalker) doList(t *schema.List) (errs ValidationErrors) { path, _ := fieldpath.MakePath(pe) // save items on the path when we shouldExtract // but ignore them when we are removing (i.e. !w.shouldExtract) - if w.toRemove.Has(path) { - if w.shouldExtract { - newItems = append(newItems, removeItemsWithSchema(item, w.toRemove, w.schema, t.ElementType, w.shouldExtract).Unstructured()) - } else { - continue + isExactPathMatch := w.toRemove.Has(path) + isPrefixMatch := !w.toRemove.WithPrefix(pe).Empty() + if w.shouldExtract { + if isPrefixMatch { + item = removeItemsWithSchema(item, w.toRemove.WithPrefix(pe), w.schema, t.ElementType, w.shouldExtract) + } + if isExactPathMatch || isPrefixMatch { + newItems = append(newItems, item.Unstructured()) } - } - if subset := w.toRemove.WithPrefix(pe); !subset.Empty() { - item = removeItemsWithSchema(item, subset, w.schema, t.ElementType, w.shouldExtract) } else { - // don't save items not on the path when we shouldExtract. - if w.shouldExtract { + if isExactPathMatch { continue } + if isPrefixMatch { + // Removing nested items within this list item and preserve if it becomes empty + hadMatches = true + wasMap := item.IsMap() + wasList := item.IsList() + item = removeItemsWithSchema(item, w.toRemove.WithPrefix(pe), w.schema, t.ElementType, w.shouldExtract) + // If item returned null but we're removing items within the structure(not the item itself), + // preserve the empty container structure + if item.IsNull() && !w.shouldExtract { + if wasMap { + item = value.NewValueInterface(map[string]interface{}{}) + } else if wasList { + item = value.NewValueInterface([]interface{}{}) + } + } + } + newItems = append(newItems, item.Unstructured()) } - newItems = append(newItems, item.Unstructured()) } - if len(newItems) > 0 { + // Preserve empty lists (non-nil) instead of converting to null when items were matched and removed + if len(newItems) > 0 || (hadMatches && !w.shouldExtract) { w.out = newItems } return nil @@ -113,6 +134,10 @@ func (w *removingWalker) doMap(t *schema.Map) ValidationErrors { } // If map is null or empty just return if m == nil || m.Empty() { + // For extraction, we just return the value as is (which is nil or empty). For extraction the difference matters. + if w.shouldExtract { + w.out = w.value.Unstructured() + } return nil } @@ -131,6 +156,7 @@ func (w *removingWalker) doMap(t *schema.Map) ValidationErrors { } newMap := map[string]interface{}{} + hadMatches := false m.Iterate(func(k string, val value.Value) bool { pe := fieldpath.PathElement{FieldName: &k} path, _ := fieldpath.MakePath(pe) @@ -148,7 +174,19 @@ func (w *removingWalker) doMap(t *schema.Map) ValidationErrors { return true } if subset := w.toRemove.WithPrefix(pe); !subset.Empty() { + hadMatches = true + wasMap := val.IsMap() + wasList := val.IsList() val = removeItemsWithSchema(val, subset, w.schema, fieldType, w.shouldExtract) + // If val returned null but we're removing items within the structure (not the field itself), + // preserve the empty container structure + if val.IsNull() && !w.shouldExtract { + if wasMap { + val = value.NewValueInterface(map[string]interface{}{}) + } else if wasList { + val = value.NewValueInterface([]interface{}{}) + } + } } else { // don't save values not on the path when we shouldExtract. if w.shouldExtract { @@ -158,7 +196,8 @@ func (w *removingWalker) doMap(t *schema.Map) ValidationErrors { newMap[k] = val.Unstructured() return true }) - if len(newMap) > 0 { + // Preserve empty maps (non-nil) instead of converting to null when items were matched and removed + if len(newMap) > 0 || (hadMatches && !w.shouldExtract) { w.out = newMap } return nil