| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- /*
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package clusterexternalsecret
- import (
- "context"
- "math/rand"
- "time"
- . "github.com/onsi/ginkgo/v2"
- . "github.com/onsi/gomega"
- v1 "k8s.io/api/core/v1"
- apierrors "k8s.io/apimachinery/pkg/api/errors"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/types"
- "sigs.k8s.io/controller-runtime/pkg/client"
- esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
- "github.com/external-secrets/external-secrets/pkg/controllers/clusterexternalsecret/cesmetrics"
- ctest "github.com/external-secrets/external-secrets/pkg/controllers/commontest"
- ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
- )
- var (
- timeout = time.Second * 10
- interval = time.Millisecond * 250
- )
- var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz")
- func RandString(n int) string {
- b := make([]rune, n)
- for i := range b {
- b[i] = letterRunes[rand.Intn(len(letterRunes))]
- }
- return string(b)
- }
- type testNamespace struct {
- namespace v1.Namespace
- containsES bool
- deletedES bool
- }
- type testCase struct {
- clusterExternalSecret *esv1beta1.ClusterExternalSecret
- // These are the namespaces that are being tested
- externalSecretNamespaces []testNamespace
- // The labels to be used for the namespaces
- namespaceLabels map[string]string
- // This is a setup function called for each test much like BeforeEach but with knowledge of the test case
- // This is used by default to create namespaces and random labels
- setup func(*testCase)
- // Is a method that's ran after everything has been created, but before the check methods are called
- beforeCheck func(*testCase)
- // A function to do any work needed before a test is ran
- preTest func()
- // checkCondition should return true if the externalSecret
- // has the expected condition
- checkCondition func(*esv1beta1.ClusterExternalSecret) bool
- // checkExternalSecret is called after the condition has been verified
- // use this to verify the externalSecret
- checkClusterExternalSecret func(*esv1beta1.ClusterExternalSecret)
- // checkExternalSecret is called after the condition has been verified
- // use this to verify the externalSecret
- checkExternalSecret func(*esv1beta1.ClusterExternalSecret, *esv1beta1.ExternalSecret)
- }
- type testTweaks func(*testCase)
- var _ = Describe("ClusterExternalSecret controller", func() {
- const (
- ClusterExternalSecretName = "test-ces"
- ExternalSecretName = "test-es"
- ExternalSecretStore = "test-store"
- ExternalSecretTargetSecretName = "test-secret"
- )
- var ExternalSecretNamespaceTargets = []testNamespace{
- {
- namespace: v1.Namespace{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-ns-1",
- },
- },
- containsES: true,
- },
- {
- namespace: v1.Namespace{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-ns-2",
- },
- },
- containsES: true,
- },
- {
- namespace: v1.Namespace{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-ns-5",
- },
- },
- containsES: false,
- },
- }
- const targetProp = "targetProperty"
- const remoteKey = "barz"
- const remoteProperty = "bang"
- makeDefaultTestCase := func() *testCase {
- return &testCase{
- checkCondition: func(ces *esv1beta1.ClusterExternalSecret) bool {
- cond := GetClusterExternalSecretCondition(ces.Status, esv1beta1.ClusterExternalSecretReady)
- if cond == nil || cond.Status != v1.ConditionTrue {
- return false
- }
- return true
- },
- checkClusterExternalSecret: func(es *esv1beta1.ClusterExternalSecret) {
- // To be implemented by the tests
- },
- checkExternalSecret: func(*esv1beta1.ClusterExternalSecret, *esv1beta1.ExternalSecret) {
- // To be implemented by the tests
- },
- clusterExternalSecret: &esv1beta1.ClusterExternalSecret{
- ObjectMeta: metav1.ObjectMeta{
- GenerateName: ClusterExternalSecretName,
- },
- Spec: esv1beta1.ClusterExternalSecretSpec{
- NamespaceSelector: metav1.LabelSelector{},
- ExternalSecretName: ExternalSecretName,
- ExternalSecretSpec: esv1beta1.ExternalSecretSpec{
- SecretStoreRef: esv1beta1.SecretStoreRef{
- Name: ExternalSecretStore,
- },
- Target: esv1beta1.ExternalSecretTarget{
- Name: ExternalSecretTargetSecretName,
- },
- Data: []esv1beta1.ExternalSecretData{
- {
- SecretKey: targetProp,
- RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
- Key: remoteKey,
- Property: remoteProperty,
- },
- },
- },
- },
- },
- },
- setup: func(tc *testCase) {
- // Generate a random label since we don't want to match previous ones.
- tc.namespaceLabels = map[string]string{
- RandString(5): RandString(5),
- }
- namespaces := []testNamespace{}
- for _, ns := range ExternalSecretNamespaceTargets {
- name, err := ctest.CreateNamespaceWithLabels(ns.namespace.Name, k8sClient, tc.namespaceLabels)
- Expect(err).ToNot(HaveOccurred())
- newNs := ns
- newNs.namespace.ObjectMeta.Name = name
- namespaces = append(namespaces, newNs)
- }
- tc.externalSecretNamespaces = namespaces
- tc.clusterExternalSecret.Spec.NamespaceSelector.MatchLabels = tc.namespaceLabels
- },
- }
- }
- // If the ES does noes not have a name specified then it should use the CES name
- syncWithoutESName := func(tc *testCase) {
- tc.clusterExternalSecret.Spec.ExternalSecretName = ""
- tc.checkExternalSecret = func(ces *esv1beta1.ClusterExternalSecret, es *esv1beta1.ExternalSecret) {
- Expect(es.ObjectMeta.Name).To(Equal(ces.ObjectMeta.Name))
- }
- }
- syncWithESMetadata := func(tc *testCase) {
- tc.clusterExternalSecret.Spec.ExternalSecretMetadata = esv1beta1.ExternalSecretMetadata{
- Labels: map[string]string{"test-label-key1": "test-label-value1", "test-label-key2": "test-label-value2"},
- Annotations: map[string]string{"test-annotation-key1": "test-annotation-value1", "test-annotation-key2": "test-annotation-value2"},
- }
- tc.checkExternalSecret = func(ces *esv1beta1.ClusterExternalSecret, es *esv1beta1.ExternalSecret) {
- Expect(es.ObjectMeta.Labels).To(Equal(map[string]string{"test-label-key1": "test-label-value1", "test-label-key2": "test-label-value2"}))
- Expect(es.ObjectMeta.Annotations).To(Equal(map[string]string{"test-annotation-key1": "test-annotation-value1", "test-annotation-key2": "test-annotation-value2"}))
- }
- }
- doNotOverwriteExistingES := func(tc *testCase) {
- tc.preTest = func() {
- es := &esv1beta1.ExternalSecret{
- ObjectMeta: metav1.ObjectMeta{
- Name: ExternalSecretName,
- Namespace: tc.externalSecretNamespaces[0].namespace.Name,
- },
- }
- err := k8sClient.Create(context.Background(), es, &client.CreateOptions{})
- Expect(err).ShouldNot(HaveOccurred())
- }
- tc.checkCondition = func(ces *esv1beta1.ClusterExternalSecret) bool {
- cond := GetClusterExternalSecretCondition(ces.Status, esv1beta1.ClusterExternalSecretPartiallyReady)
- return cond != nil
- }
- tc.checkClusterExternalSecret = func(ces *esv1beta1.ClusterExternalSecret) {
- Expect(len(ces.Status.FailedNamespaces)).Should(Equal(1))
- failure := ces.Status.FailedNamespaces[0]
- Expect(failure.Namespace).Should(Equal(tc.externalSecretNamespaces[0].namespace.Name))
- Expect(failure.Reason).Should(Equal(errSecretAlreadyExists))
- }
- }
- populatedProvisionedNamespaces := func(tc *testCase) {
- tc.checkClusterExternalSecret = func(ces *esv1beta1.ClusterExternalSecret) {
- for _, namespace := range tc.externalSecretNamespaces {
- if !namespace.containsES {
- continue
- }
- Expect(sliceContainsString(namespace.namespace.Name, ces.Status.ProvisionedNamespaces)).To(BeTrue())
- }
- }
- }
- deleteESInNonMatchingNS := func(tc *testCase) {
- tc.beforeCheck = func(tc *testCase) {
- ns := tc.externalSecretNamespaces[0]
- // Remove the labels, but leave the should contain ES so we can still check it
- ns.namespace.ObjectMeta.Labels = map[string]string{}
- tc.externalSecretNamespaces[0].deletedES = true
- err := k8sClient.Update(context.Background(), &ns.namespace, &client.UpdateOptions{})
- Expect(err).ToNot(HaveOccurred())
- time.Sleep(time.Second) // Sleep to make sure the controller gets it.
- }
- }
- syncWithMatchExpressions := func(tc *testCase) {
- tc.setup = func(tc *testCase) {
- prefixes := []string{"foo", "bar", "baz"}
- for _, prefix := range prefixes {
- labels := map[string]string{
- "e2e": "with-label-selector",
- "prefix": prefix,
- }
- ns, err := ctest.CreateNamespaceWithLabels(prefix, k8sClient, labels)
- Expect(err).ToNot(HaveOccurred())
- tc.externalSecretNamespaces = append(tc.externalSecretNamespaces, testNamespace{
- namespace: v1.Namespace{
- ObjectMeta: metav1.ObjectMeta{
- Name: ns,
- },
- },
- containsES: true,
- })
- }
- tc.clusterExternalSecret.Spec.NamespaceSelector.MatchExpressions = []metav1.LabelSelectorRequirement{
- {
- Key: "prefix",
- Operator: metav1.LabelSelectorOpIn,
- Values: prefixes,
- },
- }
- }
- tc.checkClusterExternalSecret = func(ces *esv1beta1.ClusterExternalSecret) {
- for _, namespace := range tc.externalSecretNamespaces {
- var es esv1beta1.ExternalSecret
- err := k8sClient.Get(context.Background(), types.NamespacedName{
- Namespace: namespace.namespace.Name,
- Name: ExternalSecretName,
- }, &es)
- Expect(err).ToNot(HaveOccurred())
- Expect(sliceContainsString(namespace.namespace.Name, ces.Status.ProvisionedNamespaces)).To(BeTrue())
- }
- }
- }
- DescribeTable("When reconciling a ClusterExternal Secret",
- func(tweaks ...testTweaks) {
- tc := makeDefaultTestCase()
- for _, tweak := range tweaks {
- tweak(tc)
- }
- // Run test setup
- tc.setup(tc)
- if tc.preTest != nil {
- By("running pre-test")
- tc.preTest()
- }
- ctx := context.Background()
- By("creating namespaces and cluster external secret")
- err := k8sClient.Create(ctx, tc.clusterExternalSecret)
- Expect(err).ShouldNot(HaveOccurred())
- cesKey := types.NamespacedName{Name: tc.clusterExternalSecret.Name}
- createdCES := &esv1beta1.ClusterExternalSecret{}
- By("checking the ces condition")
- Eventually(func() bool {
- err := k8sClient.Get(ctx, cesKey, createdCES)
- if err != nil {
- return false
- }
- return tc.checkCondition(createdCES)
- }, timeout, interval).Should(BeTrue())
- // Run before check
- if tc.beforeCheck != nil {
- tc.beforeCheck(tc)
- }
- tc.checkClusterExternalSecret(createdCES)
- if tc.checkExternalSecret != nil {
- for _, ns := range tc.externalSecretNamespaces {
- if !ns.containsES {
- continue
- }
- es := &esv1beta1.ExternalSecret{}
- esName := createdCES.Spec.ExternalSecretName
- if esName == "" {
- esName = createdCES.ObjectMeta.Name
- }
- esLookupKey := types.NamespacedName{
- Name: esName,
- Namespace: ns.namespace.Name,
- }
- Eventually(func() bool {
- err := k8sClient.Get(ctx, esLookupKey, es)
- if ns.deletedES && apierrors.IsNotFound(err) {
- return true
- }
- return err == nil
- }, timeout, interval).Should(BeTrue())
- tc.checkExternalSecret(createdCES, es)
- }
- }
- },
- Entry("Should use cluster external secret name if external secret name isn't defined", syncWithoutESName),
- Entry("Should set external secret metadata if the field is set", syncWithESMetadata),
- Entry("Should not overwrite existing external secrets and error out if one is present", doNotOverwriteExistingES),
- Entry("Should have list of all provisioned namespaces", populatedProvisionedNamespaces),
- Entry("Should delete external secrets when namespaces no longer match", deleteESInNonMatchingNS),
- Entry("Should sync with label selector", syncWithMatchExpressions))
- })
- func sliceContainsString(toFind string, collection []string) bool {
- for _, val := range collection {
- if val == toFind {
- return true
- }
- }
- return false
- }
- func init() {
- ctrlmetrics.SetUpLabelNames(false)
- cesmetrics.SetUpMetrics()
- }
|