diff --git a/apis/externalsecrets/v1beta1/provider_schema.go b/apis/externalsecrets/v1beta1/provider_schema.go index 8c7f37ad6..acbe069af 100644 --- a/apis/externalsecrets/v1beta1/provider_schema.go +++ b/apis/externalsecrets/v1beta1/provider_schema.go @@ -73,6 +73,9 @@ func GetProvider(s GenericStore) (Provider, error) { } spec := s.GetSpec() if spec == nil { + // Note, this condition can never be reached, because + // the Spec is not a pointer in Kubernetes. It will + // always exist. return nil, fmt.Errorf("no spec found in %#v", s) } storeName, err := getProviderName(spec.Provider) diff --git a/apis/externalsecrets/v1beta1/secretstore_types.go b/apis/externalsecrets/v1beta1/secretstore_types.go index 0ac7458b7..31346b430 100644 --- a/apis/externalsecrets/v1beta1/secretstore_types.go +++ b/apis/externalsecrets/v1beta1/secretstore_types.go @@ -50,7 +50,12 @@ type ClusterSecretStoreCondition struct { NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"` // Choose namespaces by name + // +optional Namespaces []string `json:"namespaces,omitempty"` + + // Choose namespaces by using regex matching + // +optional + NamespaceRegexes []string `json:"namespaceRegexes,omitempty"` } // SecretStoreProvider contains the provider-specific configuration. diff --git a/apis/externalsecrets/v1beta1/secretstore_validator.go b/apis/externalsecrets/v1beta1/secretstore_validator.go index aa48978a5..20da62a0a 100644 --- a/apis/externalsecrets/v1beta1/secretstore_validator.go +++ b/apis/externalsecrets/v1beta1/secretstore_validator.go @@ -16,7 +16,9 @@ package v1beta1 import ( "context" + "errors" "fmt" + "regexp" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -54,9 +56,27 @@ func (r *GenericStoreValidator) ValidateDelete(_ context.Context, _ runtime.Obje } func validateStore(store GenericStore) (admission.Warnings, error) { + if err := validateConditions(store); err != nil { + return nil, err + } + provider, err := GetProvider(store) if err != nil { return nil, err } + return provider.ValidateStore(store) } + +func validateConditions(store GenericStore) error { + var errs error + for ci, condition := range store.GetSpec().Conditions { + for ri, r := range condition.NamespaceRegexes { + if _, err := regexp.Compile(r); err != nil { + errs = errors.Join(errs, fmt.Errorf("failed to compile %dth namespace regex in %dth condition: %w", ri, ci, err)) + } + } + } + + return errs +} diff --git a/apis/externalsecrets/v1beta1/secretstore_validator_test.go b/apis/externalsecrets/v1beta1/secretstore_validator_test.go new file mode 100644 index 000000000..f146d5aa9 --- /dev/null +++ b/apis/externalsecrets/v1beta1/secretstore_validator_test.go @@ -0,0 +1,150 @@ +/* +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 v1beta1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// ValidationProvider is a simple provider that we can use without cyclic import. +type ValidationProvider struct { + Provider +} + +func (v *ValidationProvider) ValidateStore(_ GenericStore) (admission.Warnings, error) { + return nil, nil +} + +func TestValidateSecretStore(t *testing.T) { + tests := []struct { + name string + obj *SecretStore + mock func() + assertErr func(t *testing.T, err error) + }{ + { + name: "valid regex", + obj: &SecretStore{ + Spec: SecretStoreSpec{ + Conditions: []ClusterSecretStoreCondition{ + { + NamespaceRegexes: []string{`.*`}, + }, + }, + Provider: &SecretStoreProvider{ + AWS: &AWSProvider{}, + }, + }, + }, + mock: func() { + ForceRegister(&ValidationProvider{}, &SecretStoreProvider{ + AWS: &AWSProvider{}, + }) + }, + assertErr: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + { + name: "invalid regex", + obj: &SecretStore{ + Spec: SecretStoreSpec{ + Conditions: []ClusterSecretStoreCondition{ + { + NamespaceRegexes: []string{`\1`}, + }, + }, + Provider: &SecretStoreProvider{ + AWS: &AWSProvider{}, + }, + }, + }, + mock: func() { + ForceRegister(&ValidationProvider{}, &SecretStoreProvider{ + AWS: &AWSProvider{}, + }) + }, + assertErr: func(t *testing.T, err error) { + assert.EqualError(t, err, "failed to compile 0th namespace regex in 0th condition: error parsing regexp: invalid escape sequence: `\\1`") + }, + }, + { + name: "multiple errors", + obj: &SecretStore{ + Spec: SecretStoreSpec{ + Conditions: []ClusterSecretStoreCondition{ + { + NamespaceRegexes: []string{`\1`, `\2`}, + }, + }, + Provider: &SecretStoreProvider{ + AWS: &AWSProvider{}, + }, + }, + }, + mock: func() { + ForceRegister(&ValidationProvider{}, &SecretStoreProvider{ + AWS: &AWSProvider{}, + }) + }, + assertErr: func(t *testing.T, err error) { + assert.EqualError(t, err, "failed to compile 0th namespace regex in 0th condition: error parsing regexp: invalid escape sequence: `\\1`\nfailed to compile 1th namespace regex in 0th condition: error parsing regexp: invalid escape sequence: `\\2`") + }, + }, + { + name: "secret store must have only a single backend", + obj: &SecretStore{ + Spec: SecretStoreSpec{ + Provider: &SecretStoreProvider{ + AWS: &AWSProvider{}, + GCPSM: &GCPSMProvider{}, + }, + }, + }, + assertErr: func(t *testing.T, err error) { + assert.EqualError(t, err, "store error for : secret stores must only have exactly one backend specified, found 2") + }, + }, + { + name: "no registered store backend", + obj: &SecretStore{ + Spec: SecretStoreSpec{ + Conditions: []ClusterSecretStoreCondition{ + { + Namespaces: []string{"default"}, + }, + }, + }, + }, + assertErr: func(t *testing.T, err error) { + assert.EqualError(t, err, "store error for : secret stores must only have exactly one backend specified, found 0") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.mock != nil { + tt.mock() + } + + _, err := validateStore(tt.obj) + tt.assertErr(t, err) + }) + } +} diff --git a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go index 2bb5ddd6b..fae7037ea 100644 --- a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go +++ b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go @@ -682,6 +682,11 @@ func (in *ClusterSecretStoreCondition) DeepCopyInto(out *ClusterSecretStoreCondi *out = make([]string, len(*in)) copy(*out, *in) } + if in.NamespaceRegexes != nil { + in, out := &in.NamespaceRegexes, &out.NamespaceRegexes + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSecretStoreCondition. diff --git a/config/crds/bases/external-secrets.io_clustersecretstores.yaml b/config/crds/bases/external-secrets.io_clustersecretstores.yaml index b07fcfec2..58184ddae 100644 --- a/config/crds/bases/external-secrets.io_clustersecretstores.yaml +++ b/config/crds/bases/external-secrets.io_clustersecretstores.yaml @@ -1646,6 +1646,11 @@ spec: ClusterSecretStoreCondition describes a condition by which to choose namespaces to process ExternalSecrets in for a ClusterSecretStore instance. properties: + namespaceRegexes: + description: Choose namespaces by using regex matching + items: + type: string + type: array namespaceSelector: description: Choose namespace using a labelSelector properties: diff --git a/config/crds/bases/external-secrets.io_secretstores.yaml b/config/crds/bases/external-secrets.io_secretstores.yaml index 68d47d4c9..74fb9bad0 100644 --- a/config/crds/bases/external-secrets.io_secretstores.yaml +++ b/config/crds/bases/external-secrets.io_secretstores.yaml @@ -1646,6 +1646,11 @@ spec: ClusterSecretStoreCondition describes a condition by which to choose namespaces to process ExternalSecrets in for a ClusterSecretStore instance. properties: + namespaceRegexes: + description: Choose namespaces by using regex matching + items: + type: string + type: array namespaceSelector: description: Choose namespace using a labelSelector properties: diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml index fb420cfb8..5cf1dd7e9 100644 --- a/deploy/crds/bundle.yaml +++ b/deploy/crds/bundle.yaml @@ -2203,6 +2203,11 @@ spec: ClusterSecretStoreCondition describes a condition by which to choose namespaces to process ExternalSecrets in for a ClusterSecretStore instance. properties: + namespaceRegexes: + description: Choose namespaces by using regex matching + items: + type: string + type: array namespaceSelector: description: Choose namespace using a labelSelector properties: @@ -7689,6 +7694,11 @@ spec: ClusterSecretStoreCondition describes a condition by which to choose namespaces to process ExternalSecrets in for a ClusterSecretStore instance. properties: + namespaceRegexes: + description: Choose namespaces by using regex matching + items: + type: string + type: array namespaceSelector: description: Choose namespace using a labelSelector properties: diff --git a/docs/api/spec.md b/docs/api/spec.md index 69fa80ff7..b4bf821a6 100644 --- a/docs/api/spec.md +++ b/docs/api/spec.md @@ -1861,9 +1861,22 @@ Kubernetes meta/v1.LabelSelector
Choose namespaces by name
namespaceRegexes
+
+[]string
+
+Choose namespaces by using regex matching
+