Browse Source

feat: implement validating webhook

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 4 years ago
parent
commit
8fc4484cc6
52 changed files with 1903 additions and 402 deletions
  1. 1 1
      apis/externalsecrets/v1alpha1/externalsecret_conversion.go
  2. 1 1
      apis/externalsecrets/v1alpha1/externalsecret_conversion_test.go
  3. 6 2
      apis/externalsecrets/v1beta1/externalsecret_types.go
  4. 18 7
      apis/externalsecrets/v1beta1/provider.go
  5. 11 11
      apis/externalsecrets/v1beta1/provider_schema.go
  6. 32 31
      apis/externalsecrets/v1beta1/provider_schema_test.go
  7. 62 0
      apis/externalsecrets/v1beta1/secretstore_validator.go
  8. 2 0
      apis/externalsecrets/v1beta1/secretstore_webhook.go
  9. 25 2
      apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
  10. 43 23
      cmd/certcontroller.go
  11. 4 1
      cmd/root.go
  12. 42 7
      cmd/webhook.go
  13. 4 0
      config/crds/bases/external-secrets.io_externalsecrets.yaml
  14. 6 0
      deploy/charts/external-secrets/templates/cert-controller-deployment.yaml
  15. 25 0
      deploy/charts/external-secrets/templates/cert-controller-rbac.yaml
  16. 41 0
      deploy/charts/external-secrets/templates/validatingwebhook.yaml
  17. 3 2
      deploy/charts/external-secrets/templates/webhook-deployment.yaml
  18. 1 0
      deploy/charts/external-secrets/templates/webhook-secret.yaml
  19. 1 0
      deploy/charts/external-secrets/templates/webhook-service.yaml
  20. 4 0
      deploy/crds/bundle.yaml
  21. 55 55
      pkg/controllers/crds/common_test.go
  22. 99 81
      pkg/controllers/crds/crds_controller.go
  23. 7 70
      pkg/controllers/crds/crds_controller_test.go
  24. 6 13
      pkg/controllers/crds/suite_test.go
  25. 6 8
      pkg/controllers/externalsecret/externalsecret_controller.go
  26. 11 11
      pkg/controllers/externalsecret/externalsecret_controller_test.go
  27. 1 2
      pkg/controllers/secretstore/common.go
  28. 100 0
      pkg/controllers/webhookconfig/suite_test.go
  29. 186 0
      pkg/controllers/webhookconfig/webhookconfig.go
  30. 324 0
      pkg/controllers/webhookconfig/webhookconfig_test.go
  31. 7 5
      pkg/provider/akeyless/akeyless.go
  32. 6 4
      pkg/provider/alibaba/kms.go
  33. 53 5
      pkg/provider/aws/provider.go
  34. 197 0
      pkg/provider/aws/provider_test.go
  35. 4 0
      pkg/provider/aws/secretsmanager/fake/fake.go
  36. 41 5
      pkg/provider/azure/keyvault/keyvault.go
  37. 64 3
      pkg/provider/azure/keyvault/keyvault_test.go
  38. 6 4
      pkg/provider/fake/fake.go
  39. 37 4
      pkg/provider/gcp/secretmanager/secretsmanager.go
  40. 64 0
      pkg/provider/gcp/secretmanager/secretsmanager_test.go
  41. 6 4
      pkg/provider/gitlab/gitlab.go
  42. 6 4
      pkg/provider/ibm/provider.go
  43. 7 5
      pkg/provider/kubernetes/kubernetes.go
  44. 6 4
      pkg/provider/oracle/oracle.go
  45. 10 8
      pkg/provider/testing/fake/fake.go
  46. 76 6
      pkg/provider/vault/vault.go
  47. 140 0
      pkg/provider/vault/vault_test.go
  48. 6 4
      pkg/provider/webhook/webhook.go
  49. 2 3
      pkg/provider/webhook/webhook_test.go
  50. 6 4
      pkg/provider/yandex/lockbox/lockbox.go
  51. 1 2
      pkg/provider/yandex/lockbox/lockbox_test.go
  52. 31 0
      pkg/utils/utils.go

+ 1 - 1
apis/externalsecrets/v1alpha1/externalsecret_conversion.go

@@ -28,7 +28,7 @@ func (alpha *ExternalSecret) ConvertTo(betaRaw conversion.Hub) error {
 	v1beta1DataFrom := make([]esv1beta1.ExternalSecretDataFromRemoteRef, 0)
 	for _, v1alpha1RemoteRef := range alpha.Spec.DataFrom {
 		v1beta1RemoteRef := esv1beta1.ExternalSecretDataFromRemoteRef{
-			Extract: esv1beta1.ExternalSecretDataRemoteRef{
+			Extract: &esv1beta1.ExternalSecretDataRemoteRef{
 				Key:      v1alpha1RemoteRef.Key,
 				Property: v1alpha1RemoteRef.Property,
 				Version:  v1alpha1RemoteRef.Version,

+ 1 - 1
apis/externalsecrets/v1alpha1/externalsecret_conversion_test.go

@@ -183,7 +183,7 @@ func newExternalSecretV1Beta1() *esv1beta1.ExternalSecret {
 			},
 			DataFrom: []esv1beta1.ExternalSecretDataFromRemoteRef{
 				{
-					Extract: esv1beta1.ExternalSecretDataRemoteRef{
+					Extract: &esv1beta1.ExternalSecretDataRemoteRef{
 						Key:      "key",
 						Property: "property",
 						Version:  "version",

+ 6 - 2
apis/externalsecrets/v1beta1/externalsecret_types.go

@@ -161,15 +161,19 @@ type ExternalSecretDataRemoteRef struct {
 	Property string `json:"property,omitempty"`
 }
 
+// +kubebuilder:validation:MinProperties=1
+// +kubebuilder:validation:MaxProperties=1
 type ExternalSecretDataFromRemoteRef struct {
 	// Used to extract multiple key/value pairs from one secret
 	// +optional
-	Extract ExternalSecretDataRemoteRef `json:"extract,omitempty"`
+	Extract *ExternalSecretDataRemoteRef `json:"extract,omitempty"`
 	// Used to find secrets based on tags or regular expressions
 	// +optional
-	Find ExternalSecretFind `json:"find,omitempty"`
+	Find *ExternalSecretFind `json:"find,omitempty"`
 }
 
+// +kubebuilder:validation:MinProperties=1
+// +kubebuilder:validation:MaxProperties=1
 type ExternalSecretFind struct {
 	// Finds secrets based on the name.
 	// +optional

+ 18 - 7
pkg/provider/provider.go → apis/externalsecrets/v1beta1/provider.go

@@ -12,36 +12,47 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package provider
+package v1beta1
 
 import (
 	"context"
 
 	"sigs.k8s.io/controller-runtime/pkg/client"
-
-	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 )
 
+// +kubebuilder:object:root=false
+// +kubebuilder:object:generate:false
+// +k8s:deepcopy-gen:interfaces=nil
+// +k8s:deepcopy-gen=nil
+
 // Provider is a common interface for interacting with secret backends.
 type Provider interface {
 	// NewClient constructs a SecretsManager Provider
-	NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (SecretsClient, error)
+	NewClient(ctx context.Context, store GenericStore, kube client.Client, namespace string) (SecretsClient, error)
+
+	// ValidateStore checks if the provided store is valid
+	ValidateStore(store GenericStore) error
 }
 
+// +kubebuilder:object:root=false
+// +kubebuilder:object:generate:false
+// +k8s:deepcopy-gen:interfaces=nil
+// +k8s:deepcopy-gen=nil
+
 // SecretsClient provides access to secrets.
 type SecretsClient interface {
 	// GetSecret returns a single secret from the provider
-	GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error)
+	GetSecret(ctx context.Context, ref ExternalSecretDataRemoteRef) ([]byte, error)
 
 	// Validate checks if the client is configured correctly
 	// and is able to retrieve secrets from the provider
 	Validate() error
 
 	// GetSecretMap returns multiple k/v pairs from the provider
-	GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
+	GetSecretMap(ctx context.Context, ref ExternalSecretDataRemoteRef) (map[string][]byte, error)
 
 	// GetAllSecrets returns multiple k/v pairs from the provider
-	GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error)
+	GetAllSecrets(ctx context.Context, ref ExternalSecretFind) (map[string][]byte, error)
 
 	Close(ctx context.Context) error
 }

+ 11 - 11
pkg/provider/schema/schema.go → apis/externalsecrets/v1beta1/provider_schema.go

@@ -12,27 +12,24 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package schema
+package v1beta1
 
 import (
 	"encoding/json"
 	"fmt"
 	"sync"
-
-	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
 )
 
-var builder map[string]provider.Provider
+var builder map[string]Provider
 var buildlock sync.RWMutex
 
 func init() {
-	builder = make(map[string]provider.Provider)
+	builder = make(map[string]Provider)
 }
 
 // Register a store backend type. Register panics if a
 // backend with the same store is already registered.
-func Register(s provider.Provider, storeSpec *esv1beta1.SecretStoreProvider) {
+func Register(s Provider, storeSpec *SecretStoreProvider) {
 	storeName, err := getProviderName(storeSpec)
 	if err != nil {
 		panic(fmt.Sprintf("store error registering schema: %s", err.Error()))
@@ -50,7 +47,7 @@ func Register(s provider.Provider, storeSpec *esv1beta1.SecretStoreProvider) {
 
 // ForceRegister adds to store schema, overwriting a store if
 // already registered. Should only be used for testing.
-func ForceRegister(s provider.Provider, storeSpec *esv1beta1.SecretStoreProvider) {
+func ForceRegister(s Provider, storeSpec *SecretStoreProvider) {
 	storeName, err := getProviderName(storeSpec)
 	if err != nil {
 		panic(fmt.Sprintf("store error registering schema: %s", err.Error()))
@@ -62,7 +59,7 @@ func ForceRegister(s provider.Provider, storeSpec *esv1beta1.SecretStoreProvider
 }
 
 // GetProviderByName returns the provider implementation by name.
-func GetProviderByName(name string) (provider.Provider, bool) {
+func GetProviderByName(name string) (Provider, bool) {
 	buildlock.RLock()
 	f, ok := builder[name]
 	buildlock.RUnlock()
@@ -70,8 +67,11 @@ func GetProviderByName(name string) (provider.Provider, bool) {
 }
 
 // GetProvider returns the provider from the generic store.
-func GetProvider(s esv1beta1.GenericStore) (provider.Provider, error) {
+func GetProvider(s GenericStore) (Provider, error) {
 	spec := s.GetSpec()
+	if spec == nil {
+		return nil, fmt.Errorf("no spec found in %#v", s)
+	}
 	storeName, err := getProviderName(spec.Provider)
 	if err != nil {
 		return nil, fmt.Errorf("store error for %s: %w", s.GetName(), err)
@@ -90,7 +90,7 @@ func GetProvider(s esv1beta1.GenericStore) (provider.Provider, error) {
 
 // getProviderName returns the name of the configured provider
 // or an error if the provider is not configured.
-func getProviderName(storeSpec *esv1beta1.SecretStoreProvider) (string, error) {
+func getProviderName(storeSpec *SecretStoreProvider) (string, error) {
 	storeBytes, err := json.Marshal(storeSpec)
 	if err != nil || storeBytes == nil {
 		return "", fmt.Errorf("failed to marshal store spec: %w", err)

+ 32 - 31
pkg/provider/schema/schema_test.go → apis/externalsecrets/v1beta1/provider_schema_test.go

@@ -11,7 +11,7 @@ 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 schema
+package v1beta1
 
 import (
 	"context"
@@ -19,9 +19,6 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"sigs.k8s.io/controller-runtime/pkg/client"
-
-	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
 )
 
 type PP struct{}
@@ -29,22 +26,22 @@ type PP struct{}
 const shouldBeRegistered = "provider should be registered"
 
 // New constructs a SecretsManager Provider.
-func (p *PP) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
+func (p *PP) NewClient(ctx context.Context, store GenericStore, kube client.Client, namespace string) (SecretsClient, error) {
 	return p, nil
 }
 
 // GetSecret returns a single secret from the provider.
-func (p *PP) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+func (p *PP) GetSecret(ctx context.Context, ref ExternalSecretDataRemoteRef) ([]byte, error) {
 	return []byte("NOOP"), nil
 }
 
 // GetSecretMap returns multiple k/v pairs from the provider.
-func (p *PP) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+func (p *PP) GetSecretMap(ctx context.Context, ref ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 	return map[string][]byte{}, nil
 }
 
 // Empty GetAllSecrets.
-func (p *PP) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+func (p *PP) GetAllSecrets(ctx context.Context, ref ExternalSecretFind) (map[string][]byte, error) {
 	// TO be implemented
 	return map[string][]byte{}, nil
 }
@@ -57,6 +54,10 @@ func (p *PP) Validate() error {
 	return nil
 }
 
+func (p *PP) ValidateStore(store GenericStore) error {
+	return nil
+}
+
 // TestRegister tests if the Register function
 // (1) panics if it tries to register something invalid
 // (2) stores the correct provider.
@@ -66,22 +67,22 @@ func TestRegister(t *testing.T) {
 		name      string
 		expPanic  bool
 		expExists bool
-		provider  *esv1beta1.SecretStoreProvider
+		provider  *SecretStoreProvider
 	}{
 		{
 			test:      "should panic when given an invalid provider",
 			name:      "aws",
 			expPanic:  true,
 			expExists: false,
-			provider:  &esv1beta1.SecretStoreProvider{},
+			provider:  &SecretStoreProvider{},
 		},
 		{
 			test:      "should register an correct provider",
 			name:      "aws",
 			expExists: false,
-			provider: &esv1beta1.SecretStoreProvider{
-				AWS: &esv1beta1.AWSProvider{
-					Service: esv1beta1.AWSServiceSecretsManager,
+			provider: &SecretStoreProvider{
+				AWS: &AWSProvider{
+					Service: AWSServiceSecretsManager,
 				},
 			},
 		},
@@ -90,9 +91,9 @@ func TestRegister(t *testing.T) {
 			name:      "aws",
 			expPanic:  true,
 			expExists: true,
-			provider: &esv1beta1.SecretStoreProvider{
-				AWS: &esv1beta1.AWSProvider{
-					Service: esv1beta1.AWSServiceSecretsManager,
+			provider: &SecretStoreProvider{
+				AWS: &AWSProvider{
+					Service: AWSServiceSecretsManager,
 				},
 			},
 		},
@@ -109,10 +110,10 @@ func TestRegister(t *testing.T) {
 	}
 }
 
-func runTest(t *testing.T, name string, provider *esv1beta1.SecretStoreProvider, expPanic bool) {
+func runTest(t *testing.T, name string, provider *SecretStoreProvider, expPanic bool) {
 	testProvider := &PP{}
-	secretStore := &esv1beta1.SecretStore{
-		Spec: esv1beta1.SecretStoreSpec{
+	secretStore := &SecretStore{
+		Spec: SecretStoreSpec{
 			Provider: provider,
 		},
 	}
@@ -135,19 +136,19 @@ func runTest(t *testing.T, name string, provider *esv1beta1.SecretStoreProvider,
 // ForceRegister is used by other tests, we should ensure it works as expected.
 func TestForceRegister(t *testing.T) {
 	testProvider := &PP{}
-	provider := &esv1beta1.SecretStoreProvider{
-		AWS: &esv1beta1.AWSProvider{
-			Service: esv1beta1.AWSServiceParameterStore,
+	provider := &SecretStoreProvider{
+		AWS: &AWSProvider{
+			Service: AWSServiceParameterStore,
 		},
 	}
-	secretStore := &esv1beta1.SecretStore{
-		Spec: esv1beta1.SecretStoreSpec{
+	secretStore := &SecretStore{
+		Spec: SecretStoreSpec{
 			Provider: provider,
 		},
 	}
-	ForceRegister(testProvider, &esv1beta1.SecretStoreProvider{
-		AWS: &esv1beta1.AWSProvider{
-			Service: esv1beta1.AWSServiceParameterStore,
+	ForceRegister(testProvider, &SecretStoreProvider{
+		AWS: &AWSProvider{
+			Service: AWSServiceParameterStore,
 		},
 	})
 	p1, ok := GetProviderByName("aws")
@@ -164,10 +165,10 @@ func TestRegisterGCP(t *testing.T) {
 	assert.False(t, ok, "provider should not be registered")
 
 	testProvider := &PP{}
-	secretStore := &esv1beta1.SecretStore{
-		Spec: esv1beta1.SecretStoreSpec{
-			Provider: &esv1beta1.SecretStoreProvider{
-				GCPSM: &esv1beta1.GCPSMProvider{},
+	secretStore := &SecretStore{
+		Spec: SecretStoreSpec{
+			Provider: &SecretStoreProvider{
+				GCPSM: &GCPSMProvider{},
 			},
 		},
 	}

+ 62 - 0
apis/externalsecrets/v1beta1/secretstore_validator.go

@@ -0,0 +1,62 @@
+/*
+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 (
+	"context"
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+)
+
+var _ admission.CustomValidator = &GenericStoreValidator{}
+
+const (
+	errInvalidStore = "invalid store"
+)
+
+type GenericStoreValidator struct{}
+
+// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
+func (r *GenericStoreValidator) ValidateCreate(ctx context.Context, obj runtime.Object) error {
+	st, ok := obj.(GenericStore)
+	if !ok {
+		return fmt.Errorf(errInvalidStore)
+	}
+	return validateStore(st)
+}
+
+// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
+func (r *GenericStoreValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
+	st, ok := newObj.(GenericStore)
+	if !ok {
+		return fmt.Errorf(errInvalidStore)
+	}
+	return validateStore(st)
+}
+
+// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
+func (r *GenericStoreValidator) ValidateDelete(ctx context.Context, obj runtime.Object) error {
+	return nil
+}
+
+func validateStore(store GenericStore) error {
+	provider, err := GetProvider(store)
+	if err != nil {
+		return err
+	}
+	return provider.ValidateStore(store)
+}

+ 2 - 0
apis/externalsecrets/v1beta1/secretstore_webhook.go

@@ -21,11 +21,13 @@ import (
 func (c *SecretStore) SetupWebhookWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewWebhookManagedBy(mgr).
 		For(c).
+		WithValidator(&GenericStoreValidator{}).
 		Complete()
 }
 
 func (c *ClusterSecretStore) SetupWebhookWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewWebhookManagedBy(mgr).
 		For(c).
+		WithValidator(&GenericStoreValidator{}).
 		Complete()
 }

+ 25 - 2
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -422,8 +422,16 @@ func (in *ExternalSecretData) DeepCopy() *ExternalSecretData {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ExternalSecretDataFromRemoteRef) DeepCopyInto(out *ExternalSecretDataFromRemoteRef) {
 	*out = *in
-	out.Extract = in.Extract
-	in.Find.DeepCopyInto(&out.Find)
+	if in.Extract != nil {
+		in, out := &in.Extract, &out.Extract
+		*out = new(ExternalSecretDataRemoteRef)
+		**out = **in
+	}
+	if in.Find != nil {
+		in, out := &in.Find, &out.Find
+		*out = new(ExternalSecretFind)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretDataFromRemoteRef.
@@ -794,6 +802,21 @@ func (in *GCPWorkloadIdentity) DeepCopy() *GCPWorkloadIdentity {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GenericStoreValidator) DeepCopyInto(out *GenericStoreValidator) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericStoreValidator.
+func (in *GenericStoreValidator) DeepCopy() *GenericStoreValidator {
+	if in == nil {
+		return nil
+	}
+	out := new(GenericStoreValidator)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GitlabAuth) DeepCopyInto(out *GitlabAuth) {
 	*out = *in

+ 43 - 23
cmd/certcontroller.go

@@ -28,12 +28,13 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 
 	"github.com/external-secrets/external-secrets/pkg/controllers/crds"
+	"github.com/external-secrets/external-secrets/pkg/controllers/webhookconfig"
 )
 
 var certcontrollerCmd = &cobra.Command{
 	Use:   "certcontroller",
-	Short: "Controller to manage certificates for external secrets CRDs",
-	Long: `Controller to manage certificates for external secrets CRDs.
+	Short: "Controller to manage certificates for external secrets CRDs and ValidatingWebhookConfigs",
+	Long: `Controller to manage certificates for external secrets CRDs and ValidatingWebhookConfigs.
 	For more information visit https://external-secrets.io`,
 	Run: func(cmd *cobra.Command, args []string) {
 		var lvl zapcore.Level
@@ -46,11 +47,12 @@ var certcontrollerCmd = &cobra.Command{
 		ctrl.SetLogger(logger)
 
 		mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
-			Scheme:             scheme,
-			MetricsBindAddress: metricsAddr,
-			Port:               9443,
-			LeaderElection:     enableLeaderElection,
-			LeaderElectionID:   "crd-certs-controller",
+			Scheme:                 scheme,
+			MetricsBindAddress:     metricsAddr,
+			HealthProbeBindAddress: healthzAddr,
+			Port:                   9443,
+			LeaderElection:         enableLeaderElection,
+			LeaderElectionID:       "crd-certs-controller",
 			ClientDisableCacheFor: []client.Object{
 				// the client creates a ListWatch for all resource kinds that
 				// are requested with .Get().
@@ -59,31 +61,48 @@ var certcontrollerCmd = &cobra.Command{
 				// that he owns.
 				// see #721
 				&v1.Secret{},
-			}})
+			},
+		})
 		if err != nil {
 			setupLog.Error(err, "unable to start manager")
 			os.Exit(1)
 		}
-		crds := &crds.Reconciler{
-			Client:                 mgr.GetClient(),
-			Log:                    ctrl.Log.WithName("controllers").WithName("webhook-certs-updater"),
-			Scheme:                 mgr.GetScheme(),
-			SvcName:                serviceName,
-			SvcNamespace:           serviceNamespace,
-			SecretName:             secretName,
-			SecretNamespace:        secretNamespace,
-			RequeueInterval:        crdRequeueInterval,
-			CrdResources:           []string{"externalsecrets.external-secrets.io", "clustersecretstores.external-secrets.io", "secretstores.external-secrets.io"},
-			CAName:                 "external-secrets",
-			CAOrganization:         "external-secrets",
-			RestartOnSecretRefresh: false,
-		}
-		if err := crds.SetupWithManager(mgr, controller.Options{
+		crdctrl := crds.New(mgr.GetClient(), mgr.GetScheme(),
+			ctrl.Log.WithName("controllers").WithName("webhook-certs-updater"),
+			crdRequeueInterval, serviceName, serviceNamespace, secretName, secretNamespace, []string{
+				"externalsecrets.external-secrets.io",
+				"clustersecretstores.external-secrets.io",
+				"secretstores.external-secrets.io",
+			})
+		if err := crdctrl.SetupWithManager(mgr, controller.Options{
 			MaxConcurrentReconciles: concurrent,
 		}); err != nil {
 			setupLog.Error(err, errCreateController, "controller", "CustomResourceDefinition")
 			os.Exit(1)
 		}
+
+		whc := webhookconfig.New(mgr.GetClient(), mgr.GetScheme(),
+			ctrl.Log.WithName("controllers").WithName("webhook-certs-updater"),
+			serviceName, serviceNamespace,
+			secretName, secretNamespace, crdRequeueInterval)
+		if err := whc.SetupWithManager(mgr, controller.Options{
+			MaxConcurrentReconciles: concurrent,
+		}); err != nil {
+			setupLog.Error(err, errCreateController, "controller", "WebhookConfig")
+			os.Exit(1)
+		}
+
+		err = mgr.AddReadyzCheck("crd-inject", crdctrl.ReadyCheck)
+		if err != nil {
+			setupLog.Error(err, "unable to add crd readyz check")
+			os.Exit(1)
+		}
+		err = mgr.AddReadyzCheck("validation-webhook-inject", whc.ReadyCheck)
+		if err != nil {
+			setupLog.Error(err, "unable to add webhook readyz check")
+			os.Exit(1)
+		}
+
 		setupLog.Info("starting manager")
 		if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
 			setupLog.Error(err, "problem running manager")
@@ -96,6 +115,7 @@ func init() {
 	rootCmd.AddCommand(certcontrollerCmd)
 
 	certcontrollerCmd.Flags().StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
+	certcontrollerCmd.Flags().StringVar(&healthzAddr, "healthz-addr", ":8081", "The address the health endpoint binds to.")
 	certcontrollerCmd.Flags().StringVar(&serviceName, "service-name", "external-secrets-webhook", "Webhook service name")
 	certcontrollerCmd.Flags().StringVar(&serviceNamespace, "service-namespace", "default", "Webhook service namespace")
 	certcontrollerCmd.Flags().StringVar(&secretName, "secret-name", "external-secrets-webhook", "Secret to store certs for webhook")

+ 4 - 1
cmd/root.go

@@ -22,11 +22,12 @@ import (
 	"github.com/spf13/cobra"
 	"go.uber.org/zap/zapcore"
 	v1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
 
 	// To allow using gcp auth.
-	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
+	_ "k8s.io/client-go/plugin/pkg/client/auth"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -44,6 +45,7 @@ var (
 	dnsName                       string
 	certDir                       string
 	metricsAddr                   string
+	healthzAddr                   string
 	controllerClass               string
 	enableLeaderElection          bool
 	concurrent                    int
@@ -64,6 +66,7 @@ func init() {
 	_ = clientgoscheme.AddToScheme(scheme)
 	_ = esv1beta1.AddToScheme(scheme)
 	_ = esv1alpha1.AddToScheme(scheme)
+	_ = apiextensionsv1.AddToScheme(scheme)
 }
 
 var rootCmd = &cobra.Command{

+ 42 - 7
cmd/webhook.go

@@ -17,6 +17,7 @@ package cmd
 
 import (
 	"context"
+	"net/http"
 	"os"
 	"os/signal"
 	"syscall"
@@ -65,12 +66,12 @@ var webhookCmd = &cobra.Command{
 		logger := zap.New(zap.Level(lvl))
 		ctrl.SetLogger(logger)
 
-		setupLog.Info("validating certs")
-		err = crds.CheckCerts(c, dnsName, time.Now().Add(time.Hour))
+		err = waitForCerts(c, time.Minute*2)
 		if err != nil {
-			setupLog.Error(err, "error checking certs")
+			setupLog.Error(err, "unable to validate certificates")
 			os.Exit(1)
 		}
+
 		ctx, cancel := context.WithCancel(context.Background())
 		go func(c crds.CertInfo, dnsName string, every time.Duration) {
 			sigs := make(chan os.Signal, 1)
@@ -92,10 +93,11 @@ var webhookCmd = &cobra.Command{
 		}(c, dnsName, certCheckInterval)
 
 		mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
-			Scheme:             scheme,
-			MetricsBindAddress: metricsAddr,
-			Port:               9443,
-			CertDir:            certDir,
+			Scheme:                 scheme,
+			MetricsBindAddress:     metricsAddr,
+			HealthProbeBindAddress: healthzAddr,
+			Port:                   9443,
+			CertDir:                certDir,
 		})
 		if err != nil {
 			setupLog.Error(err, "unable to start manager")
@@ -125,6 +127,15 @@ var webhookCmd = &cobra.Command{
 			setupLog.Error(err, errCreateWebhook, "webhook", "ClusterSecretStore-v1alpha1")
 			os.Exit(1)
 		}
+
+		err = mgr.AddReadyzCheck("certs", func(_ *http.Request) error {
+			return crds.CheckCerts(c, dnsName, time.Now().Add(time.Hour))
+		})
+		if err != nil {
+			setupLog.Error(err, "unable to add certs readyz check")
+			os.Exit(1)
+		}
+
 		setupLog.Info("starting manager")
 		if err := mgr.Start(ctx); err != nil {
 			setupLog.Error(err, "problem running manager")
@@ -133,9 +144,33 @@ var webhookCmd = &cobra.Command{
 	},
 }
 
+// waitForCerts waits until the certificates become ready.
+// If they don't become ready within a given time duration
+// this function returns an error.
+// certs are generated by the certcontroller.
+func waitForCerts(c crds.CertInfo, timeout time.Duration) error {
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	for {
+		setupLog.Info("validating certs")
+		err := crds.CheckCerts(c, dnsName, time.Now().Add(time.Hour))
+		if err == nil {
+			return nil
+		}
+		if err != nil {
+			setupLog.Error(err, "invalid certs. retrying...")
+			<-time.After(time.Second * 10)
+		}
+		if ctx.Err() != nil {
+			return ctx.Err()
+		}
+	}
+}
+
 func init() {
 	rootCmd.AddCommand(webhookCmd)
 	webhookCmd.Flags().StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
+	webhookCmd.Flags().StringVar(&healthzAddr, "healthz-addr", ":8081", "The address the health endpoint binds to.")
 	webhookCmd.Flags().StringVar(&dnsName, "dns-name", "localhost", "DNS name to validate certificates with")
 	webhookCmd.Flags().StringVar(&certDir, "cert-dir", "/tmp/k8s-webhook-server/serving-certs", "path to check for certs")
 	webhookCmd.Flags().StringVar(&loglevel, "loglevel", "info", "loglevel to use, one of: debug, info, warn, error, dpanic, panic, fatal")

+ 4 - 0
config/crds/bases/external-secrets.io_externalsecrets.yaml

@@ -320,6 +320,8 @@ spec:
                   Provider data If multiple entries are specified, the Secret keys
                   are merged in the specified order
                 items:
+                  maxProperties: 1
+                  minProperties: 1
                   properties:
                     extract:
                       description: Used to extract multiple key/value pairs from one
@@ -341,6 +343,8 @@ spec:
                       type: object
                     find:
                       description: Used to find secrets based on tags or regular expressions
+                      maxProperties: 1
+                      minProperties: 1
                       properties:
                         name:
                           description: Finds secrets based on the name.

+ 6 - 0
deploy/charts/external-secrets/templates/cert-controller-deployment.yaml

@@ -61,6 +61,12 @@ spec:
             - containerPort: {{ .Values.certController.prometheus.service.port }}
               protocol: TCP
               name: metrics
+          readinessProbe:
+            httpGet:
+              port: 8081
+              path: /readyz
+            initialDelaySeconds: 20
+            periodSeconds: 5
           {{- with .Values.certController.extraEnv }}
           env:
             {{- toYaml . | nindent 12 }}

+ 25 - 0
deploy/charts/external-secrets/templates/cert-controller-rbac.yaml

@@ -16,6 +16,31 @@ rules:
     - "watch"
     - "update"
     - "patch"
+  - apiGroups:
+    - "admissionregistration.k8s.io"
+    resources:
+    - "validatingwebhookconfigurations"
+    verbs:
+    - "get"
+    - "list"
+    - "watch"
+    - "update"
+    - "patch"
+  - apiGroups:
+    - ""
+    resources:
+    - "endpoints"
+    verbs:
+    - "list"
+    - "get"
+    - "watch"
+  - apiGroups:
+    - ""
+    resources:
+    - "events"
+    verbs:
+    - "create"
+    - "patch"
   - apiGroups:
     - ""
     resources:

+ 41 - 0
deploy/charts/external-secrets/templates/validatingwebhook.yaml

@@ -0,0 +1,41 @@
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+  name: secretstore-validate
+  labels:
+    external-secrets.io/component: webhook
+webhooks:
+- name: "validate.secretstore.external-secrets.io"
+  rules:
+  - apiGroups:   ["external-secrets.io"]
+    apiVersions: ["v1beta1"]
+    operations:  ["CREATE", "UPDATE", "DELETE"]
+    resources:   ["secretstores"]
+    scope:       "Namespaced"
+  clientConfig:
+    service:
+      namespace: {{ .Release.Namespace | quote }}
+      name: {{ include "external-secrets.fullname" . }}-webhook
+      path: /validate-external-secrets-io-v1beta1-secretstore
+    # will be set by controller
+    caBundle: Cg==
+  admissionReviewVersions: ["v1", "v1beta1"]
+  sideEffects: None
+  timeoutSeconds: 5
+
+- name: "validate.clustersecretstore.external-secrets.io"
+  rules:
+  - apiGroups:   ["external-secrets.io"]
+    apiVersions: ["v1beta1"]
+    operations:  ["CREATE", "UPDATE", "DELETE"]
+    resources:   ["clustersecretstores"]
+    scope:       "Cluster"
+  clientConfig:
+    service:
+      namespace: {{ .Release.Namespace | quote }}
+      name: {{ include "external-secrets.fullname" . }}-webhook
+      path: /validate-external-secrets-io-v1beta1-clustersecretstore
+    caBundle: Cg== # will be set by controller
+  admissionReviewVersions: ["v1", "v1beta1"]
+  sideEffects: None
+  timeoutSeconds: 5

+ 3 - 2
deploy/charts/external-secrets/templates/webhook-deployment.yaml

@@ -63,8 +63,9 @@ spec:
               protocol: TCP
               name: webhook
           readinessProbe:
-            tcpSocket:
-              port: 9443
+            httpGet:
+              port: 8081
+              path: /readyz
             initialDelaySeconds: 20
             periodSeconds: 5
           {{- with .Values.webhook.extraEnv }}

+ 1 - 0
deploy/charts/external-secrets/templates/webhook-secret.yaml

@@ -2,6 +2,7 @@ apiVersion: v1
 kind: Secret
 metadata:
   name: {{ include "external-secrets.fullname" . }}-webhook
+  namespace: {{ .Release.Namespace | quote }}
   labels:
     {{- include "external-secrets-webhook.labels" . | nindent 4 }}
     external-secrets.io/component : webhook

+ 1 - 0
deploy/charts/external-secrets/templates/webhook-service.yaml

@@ -2,6 +2,7 @@ apiVersion: v1
 kind: Service
 metadata:
   name: {{ include "external-secrets.fullname" . }}-webhook
+  namespace: {{ .Release.Namespace | quote }}
   labels:
     {{- include "external-secrets-webhook.labels" . | nindent 4 }}
     external-secrets.io/component : webhook

+ 4 - 0
deploy/crds/bundle.yaml

@@ -2244,6 +2244,8 @@ spec:
                 dataFrom:
                   description: DataFrom is used to fetch all properties from a specific Provider data If multiple entries are specified, the Secret keys are merged in the specified order
                   items:
+                    maxProperties: 1
+                    minProperties: 1
                     properties:
                       extract:
                         description: Used to extract multiple key/value pairs from one secret
@@ -2262,6 +2264,8 @@ spec:
                         type: object
                       find:
                         description: Used to find secrets based on tags or regular expressions
+                        maxProperties: 1
+                        minProperties: 1
                         properties:
                           name:
                             description: Finds secrets based on the name.

+ 55 - 55
pkg/controllers/crds/common_test.go

@@ -15,28 +15,22 @@ package crds
 
 import (
 	"context"
-	"encoding/json"
 	"time"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
 	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
-)
-
-const (
-	crdGroup   = "apiextensions.k8s.io"
-	crdKind    = "CustomResourceDefinition"
-	crdVersion = "v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 )
 
 type testCase struct {
-	crd     unstructured.Unstructured
-	crd2    unstructured.Unstructured
+	crd     *apiextensions.CustomResourceDefinition
+	crd2    *apiextensions.CustomResourceDefinition
 	service corev1.Service
 	secret  corev1.Secret
 	assert  func()
@@ -50,31 +44,27 @@ var _ = Describe("CRD reconcile", func() {
 	})
 
 	AfterEach(func() {
-		// To improve later on with proper clean up.
+		ctx := context.Background()
+		k8sClient.Delete(ctx, &test.secret)
+		k8sClient.Delete(ctx, &test.service)
+		deleteCRD(test.crd)
+		deleteCRD(test.crd2)
 	})
 
 	// a invalid provider config should be reflected
 	// in the store status condition
 	PatchesCRD := func(tc *testCase) {
 		tc.assert = func() {
-			Consistently(func() bool {
-				ss := unstructured.Unstructured{}
-				ss.SetGroupVersionKind(schema.GroupVersionKind{Kind: crdKind, Version: crdVersion, Group: crdGroup})
+			Eventually(func() bool {
+				crd := &apiextensions.CustomResourceDefinition{}
 				err := k8sClient.Get(context.Background(), types.NamespacedName{
 					Name: "secretstores.test.io",
-				}, &ss)
+				}, crd)
 				if err != nil {
 					return false
 				}
-				val, ok, err := unstructured.NestedString(ss.Object, "spec", "conversion", "webhook", "clientConfig", "service", "name")
-				if err != nil || !ok {
-					return false
-				}
-				want, ok, err := unstructured.NestedString(tc.crd.Object, "spec", "conversion", "webhook", "clientConfig", "service", "name")
-				if err != nil || !ok {
-					return false
-				}
-				return want != val
+				return crd.Spec.Conversion.Webhook.ClientConfig.Service.Name !=
+					tc.crd.Spec.Conversion.Webhook.ClientConfig.Service.Name
 			}).
 				WithTimeout(time.Second * 10).
 				WithPolling(time.Second).
@@ -87,23 +77,15 @@ var _ = Describe("CRD reconcile", func() {
 	ignoreNonTargetCRDs := func(tc *testCase) {
 		tc.assert = func() {
 			Consistently(func() bool {
-				ss := unstructured.Unstructured{}
-				ss.SetGroupVersionKind(schema.GroupVersionKind{Kind: crdKind, Version: crdVersion, Group: crdGroup})
+				crd := &apiextensions.CustomResourceDefinition{}
 				err := k8sClient.Get(context.Background(), types.NamespacedName{
 					Name: "some-other.test.io",
-				}, &ss)
+				}, crd)
 				if err != nil {
 					return false
 				}
-				got, ok, err := unstructured.NestedString(ss.Object, "spec", "conversion", "webhook", "clientConfig", "service", "name")
-				if !ok || err != nil {
-					return false
-				}
-				want, ok, err := unstructured.NestedString(tc.crd2.Object, "spec", "conversion", "webhook", "clientConfig", "service", "name")
-				if !ok || err != nil {
-					return false
-				}
-				return got == want
+				return crd.Spec.Conversion.Webhook.ClientConfig.Service.Name ==
+					tc.crd2.Spec.Conversion.Webhook.ClientConfig.Service.Name
 			}).
 				WithTimeout(time.Second * 3).
 				WithPolling(time.Millisecond * 500).
@@ -116,21 +98,47 @@ var _ = Describe("CRD reconcile", func() {
 			mut(test)
 		}
 		ctx := context.Background()
-		k8sClient.Create(ctx, &test.secret)
-		k8sClient.Create(ctx, &test.service)
-		k8sClient.Create(ctx, &test.crd)
-		k8sClient.Create(ctx, &test.crd2)
+		err := k8sClient.Create(ctx, &test.secret)
+		Expect(err).ToNot(HaveOccurred())
+		err = k8sClient.Create(ctx, &test.service)
+		Expect(err).ToNot(HaveOccurred())
+		err = k8sClient.Create(ctx, test.crd)
+		Expect(err).ToNot(HaveOccurred())
+		err = k8sClient.Create(ctx, test.crd2)
+		Expect(err).ToNot(HaveOccurred())
 		test.assert()
 	},
-
 		Entry("[namespace] Ignore non Target CRDs", ignoreNonTargetCRDs),
 		Entry("[namespace] Patch target CRDs", PatchesCRD),
 	)
 
 })
 
-func makeUnstructuredCRD(plural, group string) unstructured.Unstructured {
-	crd := apiextensions.CustomResourceDefinition{
+func deleteCRD(crd *apiextensions.CustomResourceDefinition) {
+	err := k8sClient.Delete(context.Background(), crd, client.GracePeriodSeconds(0))
+	if err != nil && !apierrors.IsNotFound(err) {
+		Fail("unable to delete crd " + crd.Name)
+		return
+	}
+	Eventually(func() bool {
+		err := k8sClient.Get(context.Background(), types.NamespacedName{
+			Name: crd.Name,
+		}, crd)
+		if err == nil {
+			// force delete by removing finalizers
+			// note: we can not delete a CRD with an invalid caBundle field
+			cpy := crd.DeepCopy()
+			controllerutil.RemoveFinalizer(cpy, "customresourcecleanup.apiextensions.k8s.io")
+			p := client.MergeFrom(crd)
+			k8sClient.Patch(context.Background(), cpy, p)
+			return false
+		}
+		return apierrors.IsNotFound(err)
+	}).WithTimeout(time.Second * 5).WithPolling(time.Second).Should(BeTrue())
+}
+
+func makeCRD(plural, group string) *apiextensions.CustomResourceDefinition {
+	return &apiextensions.CustomResourceDefinition{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: plural + "." + group,
 		},
@@ -160,7 +168,7 @@ func makeUnstructuredCRD(plural, group string) unstructured.Unstructured {
 				Webhook: &apiextensions.WebhookConversion{
 					ConversionReviewVersions: []string{"v1"},
 					ClientConfig: &apiextensions.WebhookClientConfig{
-						CABundle: []byte("foobar"),
+						CABundle: []byte(`Cg==`),
 						Service: &apiextensions.ServiceReference{
 							Name:      "webhook",
 							Namespace: "default",
@@ -170,14 +178,6 @@ func makeUnstructuredCRD(plural, group string) unstructured.Unstructured {
 			},
 		},
 	}
-	marshal, _ := json.Marshal(crd)
-	unmarshal := make(map[string]interface{})
-	json.Unmarshal(marshal, &unmarshal)
-	u := unstructured.Unstructured{
-		Object: unmarshal,
-	}
-	u.SetGroupVersionKind(schema.GroupVersionKind{Kind: crdKind, Version: "v1", Group: crdGroup})
-	return u
 }
 
 func makeSecret() corev1.Secret {
@@ -217,8 +217,8 @@ func makeDefaultTestcase() *testCase {
 		assert: func() {
 			// this is a noop by default
 		},
-		crd:     makeUnstructuredCRD("secretstores", "test.io"),
-		crd2:    makeUnstructuredCRD("some-other", "test.io"),
+		crd:     makeCRD("secretstores", "test.io"),
+		crd2:    makeCRD("some-other", "test.io"),
 		secret:  makeSecret(),
 		service: makeService(),
 	}

+ 99 - 81
pkg/controllers/crds/crds_controller.go

@@ -22,19 +22,20 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509/pkix"
-	"encoding/base64"
 	"encoding/pem"
 	"errors"
 	"fmt"
 	"math/big"
+	"net/http"
 	"os"
+	"path/filepath"
+	"sync"
 	"time"
 
 	"github.com/go-logr/logr"
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/tools/record"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -49,31 +50,49 @@ const (
 	caKeyName            = "ca.key"
 	certValidityDuration = 10 * 365 * 24 * time.Hour
 	LookaheadInterval    = 90 * 24 * time.Hour
-)
-
-type WebhookType int
 
-const (
-	Validating WebhookType = iota
-	Mutating
-	CRDConversion
+	errResNotReady       = "resource not ready: %s"
+	errSubsetsNotReady   = "subsets not ready"
+	errAddressesNotReady = "addresses not ready"
 )
 
 type Reconciler struct {
 	client.Client
-	Log                    logr.Logger
-	Scheme                 *runtime.Scheme
-	recorder               record.EventRecorder
-	SvcName                string
-	SvcNamespace           string
-	SecretName             string
-	SecretNamespace        string
-	CrdResources           []string
-	dnsName                string
-	CAName                 string
-	CAOrganization         string
-	RestartOnSecretRefresh bool
-	RequeueInterval        time.Duration
+	Log             logr.Logger
+	Scheme          *runtime.Scheme
+	recorder        record.EventRecorder
+	SvcName         string
+	SvcNamespace    string
+	SecretName      string
+	SecretNamespace string
+	CrdResources    []string
+	dnsName         string
+	CAName          string
+	CAOrganization  string
+	RequeueInterval time.Duration
+
+	// the controller is ready when all crds are injected
+	rdyMu          *sync.Mutex
+	readyStatusMap map[string]bool
+}
+
+func New(k8sClient client.Client, scheme *runtime.Scheme, logger logr.Logger,
+	interval time.Duration, svcName, svcNamespace, secretName, secretNamespace string, resources []string) *Reconciler {
+	return &Reconciler{
+		Client:          k8sClient,
+		Log:             logger,
+		Scheme:          scheme,
+		SvcName:         svcName,
+		SvcNamespace:    svcNamespace,
+		SecretName:      secretName,
+		SecretNamespace: secretNamespace,
+		RequeueInterval: interval,
+		CrdResources:    resources,
+		CAName:          "external-secrets",
+		CAOrganization:  "external-secrets",
+		rdyMu:           &sync.Mutex{},
+		readyStatusMap:  map[string]bool{},
+	}
 }
 
 type CertInfo struct {
@@ -82,10 +101,6 @@ type CertInfo struct {
 	KeyName  string
 	CAName   string
 }
-type WebhookInfo struct {
-	Name string
-	Type WebhookType
-}
 
 func contains(s []string, e string) bool {
 	for _, a := range s {
@@ -95,44 +110,62 @@ func contains(s []string, e string) bool {
 	}
 	return false
 }
+
 func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	log := r.Log.WithValues("CustomResourceDefinition", req.NamespacedName)
 	if contains(r.CrdResources, req.NamespacedName.Name) {
 		err := r.updateCRD(ctx, req)
 		if err != nil {
 			log.Error(err, "failed to inject conversion webhook")
+			r.rdyMu.Lock()
+			r.readyStatusMap[req.NamespacedName.Name] = false
+			r.rdyMu.Unlock()
 			return ctrl.Result{}, err
 		}
+		r.rdyMu.Lock()
+		r.readyStatusMap[req.NamespacedName.Name] = true
+		r.rdyMu.Unlock()
 	}
 	return ctrl.Result{RequeueAfter: r.RequeueInterval}, nil
 }
 
-func (r *Reconciler) ConvertToWebhookInfo() []WebhookInfo {
-	info := make([]WebhookInfo, len(r.CrdResources))
-	for p, v := range r.CrdResources {
-		r := WebhookInfo{
-			Name: v,
-			Type: CRDConversion,
+// ReadyCheck reviews if all webhook configs have been injected into the CRDs
+// and if the referenced webhook service is ready.
+func (r *Reconciler) ReadyCheck(_ *http.Request) error {
+	for _, res := range r.CrdResources {
+		r.rdyMu.Lock()
+		rdy := r.readyStatusMap[res]
+		r.rdyMu.Unlock()
+		if !rdy {
+			return fmt.Errorf(errResNotReady, res)
 		}
-		info[p] = r
 	}
-	return info
+	var eps corev1.Endpoints
+	err := r.Get(context.TODO(), types.NamespacedName{
+		Name:      r.SvcName,
+		Namespace: r.SvcNamespace,
+	}, &eps)
+	if err != nil {
+		return err
+	}
+	if len(eps.Subsets) == 0 {
+		return fmt.Errorf(errSubsetsNotReady)
+	}
+	if len(eps.Subsets[0].Addresses) == 0 {
+		return fmt.Errorf(errAddressesNotReady)
+	}
+	return nil
 }
 
 func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
-	crdGVK := schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"}
-	res := &unstructured.Unstructured{}
-	res.SetGroupVersionKind(crdGVK)
 	r.recorder = mgr.GetEventRecorderFor("custom-resource-definition")
 	return ctrl.NewControllerManagedBy(mgr).
 		WithOptions(opts).
-		For(res).
+		For(&apiext.CustomResourceDefinition{}).
 		Complete(r)
 }
 
 func (r *Reconciler) updateCRD(ctx context.Context, req ctrl.Request) error {
-	crdGVK := schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"}
-
 	secret := corev1.Secret{}
 	secretName := types.NamespacedName{
 		Name:      r.SecretName,
@@ -142,16 +175,15 @@ func (r *Reconciler) updateCRD(ctx context.Context, req ctrl.Request) error {
 	if err != nil {
 		return err
 	}
-	updatedResource := &unstructured.Unstructured{}
-	updatedResource.SetGroupVersionKind(crdGVK)
-	if err := r.Get(ctx, req.NamespacedName, updatedResource); err != nil {
+	var updatedResource apiext.CustomResourceDefinition
+	if err := r.Get(ctx, req.NamespacedName, &updatedResource); err != nil {
 		return err
 	}
 	svc := types.NamespacedName{
 		Name:      r.SvcName,
 		Namespace: r.SvcNamespace,
 	}
-	if err := injectSvcToConversionWebhook(updatedResource, svc); err != nil {
+	if err := injectService(&updatedResource, svc); err != nil {
 		return err
 	}
 	r.dnsName = fmt.Sprintf("%v.%v.svc", r.SvcName, r.SvcNamespace)
@@ -164,45 +196,35 @@ func (r *Reconciler) updateCRD(ctx context.Context, req ctrl.Request) error {
 		if err != nil {
 			return err
 		}
-		if err := injectCertToConversionWebhook(updatedResource, artifacts.CertPEM); err != nil {
+		if err := injectCert(&updatedResource, artifacts.CertPEM); err != nil {
 			return err
 		}
 	}
-	if err := r.Update(ctx, updatedResource); err != nil {
+	if err := r.Update(ctx, &updatedResource); err != nil {
 		return err
 	}
 	return nil
 }
 
-func injectSvcToConversionWebhook(crd *unstructured.Unstructured, svc types.NamespacedName) error {
-	_, found, err := unstructured.NestedMap(crd.Object, "spec", "conversion", "webhook", "clientConfig")
-	if err != nil {
-		return err
-	}
-	if !found {
-		return errors.New("`conversion.webhook.clientConfig` field not found in CustomResourceDefinition")
-	}
-	if err := unstructured.SetNestedField(crd.Object, svc.Name, "spec", "conversion", "webhook", "clientConfig", "service", "name"); err != nil {
-		return err
-	}
-	if err := unstructured.SetNestedField(crd.Object, svc.Namespace, "spec", "conversion", "webhook", "clientConfig", "service", "namespace"); err != nil {
-		return err
+func injectService(crd *apiext.CustomResourceDefinition, svc types.NamespacedName) error {
+	if crd.Spec.Conversion == nil ||
+		crd.Spec.Conversion.Webhook == nil ||
+		crd.Spec.Conversion.Webhook.ClientConfig == nil ||
+		crd.Spec.Conversion.Webhook.ClientConfig.Service == nil {
+		return fmt.Errorf("unexpected crd conversion webhook config")
 	}
+	crd.Spec.Conversion.Webhook.ClientConfig.Service.Namespace = svc.Namespace
+	crd.Spec.Conversion.Webhook.ClientConfig.Service.Name = svc.Name
 	return nil
 }
 
-func injectCertToConversionWebhook(crd *unstructured.Unstructured, certPem []byte) error {
-	_, found, err := unstructured.NestedMap(crd.Object, "spec", "conversion", "webhook", "clientConfig")
-	if err != nil {
-		return err
-	}
-	if !found {
-		return errors.New("`conversion.webhook.clientConfig` field not found in CustomResourceDefinition")
-	}
-	if err := unstructured.SetNestedField(crd.Object, base64.StdEncoding.EncodeToString(certPem), "spec", "conversion", "webhook", "clientConfig", "caBundle"); err != nil {
-		return err
+func injectCert(crd *apiext.CustomResourceDefinition, certPem []byte) error {
+	if crd.Spec.Conversion == nil ||
+		crd.Spec.Conversion.Webhook == nil ||
+		crd.Spec.Conversion.Webhook.ClientConfig == nil {
+		return fmt.Errorf("unexpected crd conversion webhook config")
 	}
-
+	crd.Spec.Conversion.Webhook.ClientConfig.CABundle = certPem
 	return nil
 }
 
@@ -289,18 +311,12 @@ func (r *Reconciler) refreshCertIfNeeded(secret *corev1.Secret) (bool, error) {
 		if err := r.refreshCerts(true, secret); err != nil {
 			return false, err
 		}
-		if r.RestartOnSecretRefresh {
-			os.Exit(0)
-		}
 		return true, nil
 	}
 	if !r.validServerCert(secret.Data[caCertName], secret.Data[certName], secret.Data[keyName]) {
 		if err := r.refreshCerts(false, secret); err != nil {
 			return false, err
 		}
-		if r.RestartOnSecretRefresh {
-			os.Exit(0)
-		}
 		return true, nil
 	}
 	return true, nil
@@ -450,21 +466,23 @@ func (r *Reconciler) writeSecret(cert, key []byte, caArtifacts *KeyPairArtifacts
 	return r.Update(context.Background(), secret)
 }
 
+// CheckCerts verifies that certificates exist in a given fs location
+// and if they're valid.
 func CheckCerts(c CertInfo, dnsName string, at time.Time) error {
-	certFile := c.CertDir + "/" + c.CertName
+	certFile := filepath.Join(c.CertDir, c.CertName)
 	_, err := os.Stat(certFile)
 	if err != nil {
 		return err
 	}
-	ca, err := os.ReadFile(c.CertDir + "/" + c.CAName)
+	ca, err := os.ReadFile(filepath.Join(c.CertDir, c.CAName))
 	if err != nil {
 		return err
 	}
-	cert, err := os.ReadFile(c.CertDir + "/" + c.CertName)
+	cert, err := os.ReadFile(filepath.Join(c.CertDir, c.CertName))
 	if err != nil {
 		return err
 	}
-	key, err := os.ReadFile(c.CertDir + "/" + c.KeyName)
+	key, err := os.ReadFile(filepath.Join(c.CertDir, c.KeyName))
 	if err != nil {
 		return err
 	}

+ 7 - 70
pkg/controllers/crds/crds_controller_test.go

@@ -18,7 +18,6 @@ import (
 	"context"
 	"crypto/rsa"
 	"crypto/x509"
-	"encoding/json"
 	"os"
 	"testing"
 	"time"
@@ -26,15 +25,12 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
 	client "sigs.k8s.io/controller-runtime/pkg/client/fake"
 )
 
 const (
-	setupError              = "Could not setup test"
-	errorSearchingField     = "Error when searching for field"
 	failedCreateCaCerts     = "could not create ca certificates:%v"
 	failedCreateServerCerts = "could not create server certificates:%v"
 	invalidCerts            = "generated certificates are invalid:%v,%v"
@@ -92,21 +88,6 @@ func newCRD() apiextensionsv1.CustomResourceDefinition {
 		},
 	}
 }
-func TestConvertToWebhookInfo(t *testing.T) {
-	rec := newReconciler()
-	info := rec.ConvertToWebhookInfo()
-	if len(info) != 3 {
-		t.Errorf("Convert to WebhookInfo failed. Total resources:%d", len(info))
-	}
-	for _, v := range info {
-		if v.Type != CRDConversion {
-			t.Errorf("Convert to WebhookInfo failed. wrong type:%v", v.Type)
-		}
-		if v.Name != "one" && v.Name != "two" && v.Name != "three" {
-			t.Errorf("Convert to WebhookInfo failed. wrong name:%v", v.Name)
-		}
-	}
-}
 
 func TestUpdateCRD(t *testing.T) {
 	rec := newReconciler()
@@ -123,51 +104,26 @@ func TestUpdateCRD(t *testing.T) {
 	}
 	err := rec.updateCRD(ctx, req)
 	if err != nil {
-		t.Errorf("Failed updating CRD:%v", err)
+		t.Errorf("Failed updating CRD: %v", err)
 	}
 }
 
 func TestInjectSvcToConversionWebhook(t *testing.T) {
 	svc := newService()
 	crd := newCRD()
-	crdunmarshalled := make(map[string]interface{})
-	crdJSON, err := json.Marshal(crd)
-	if err != nil {
-		t.Fatal(setupError)
-	}
-	err = json.Unmarshal(crdJSON, &crdunmarshalled)
-	if err != nil {
-		t.Fatal(setupError)
-	}
-	u := unstructured.Unstructured{
-		Object: crdunmarshalled,
-	}
 	name := types.NamespacedName{
 		Name:      svc.Name,
 		Namespace: svc.Namespace,
 	}
-	err = injectSvcToConversionWebhook(&u, name)
+	err := injectService(&crd, name)
 	if err != nil {
 		t.Errorf("Failed: error when injecting: %v", err)
 	}
-	val, found, err := unstructured.NestedString(u.Object, "spec", "conversion", "webhook", "clientConfig", "service", "name")
-	if err != nil {
-		t.Error(errorSearchingField)
-	}
-	if !found {
-		t.Error("fieldNotFound")
-	}
+	val := crd.Spec.Conversion.Webhook.ClientConfig.Service.Name
 	if val != "foo" {
 		t.Errorf("Wrong service name injected: %v", val)
 	}
-
-	val, found, err = unstructured.NestedString(u.Object, "spec", "conversion", "webhook", "clientConfig", "service", "namespace")
-	if err != nil {
-		t.Error(errorSearchingField)
-	}
-	if !found {
-		t.Error("fieldNotFound")
-	}
+	val = crd.Spec.Conversion.Webhook.ClientConfig.Service.Namespace
 	if val != "default" {
 		t.Errorf("Wrong service namespace injected: %v", val)
 	}
@@ -176,31 +132,12 @@ func TestInjectSvcToConversionWebhook(t *testing.T) {
 func TestInjectCertToConversionWebhook(t *testing.T) {
 	certPEM := []byte("foobar")
 	crd := newCRD()
-	crdunmarshalled := make(map[string]interface{})
-	crdJSON, err := json.Marshal(crd)
-	if err != nil {
-		t.Fatal(setupError)
-	}
-	err = json.Unmarshal(crdJSON, &crdunmarshalled)
-	if err != nil {
-		t.Fatal(setupError)
-	}
-	u := unstructured.Unstructured{
-		Object: crdunmarshalled,
-	}
-	err = injectCertToConversionWebhook(&u, certPEM)
+	err := injectCert(&crd, certPEM)
 	if err != nil {
 		t.Errorf("Failed: error when injecting: %v", err)
 	}
-	val, found, err := unstructured.NestedString(u.Object, "spec", "conversion", "webhook", "clientConfig", "caBundle")
-	if err != nil {
-		t.Error(errorSearchingField)
-	}
-	if !found {
-		t.Error("fieldNotFound")
-	}
-	if val != "Zm9vYmFy" {
-		t.Errorf("Wrong certificate name injected: %v", val)
+	if string(crd.Spec.Conversion.Webhook.ClientConfig.CABundle) != "foobar" {
+		t.Errorf("Wrong certificate name injected: %v", string(crd.Spec.Conversion.Webhook.ClientConfig.CABundle))
 	}
 }
 func TestPopulateSecret(t *testing.T) {

+ 6 - 13
pkg/controllers/crds/suite_test.go

@@ -18,6 +18,7 @@ import (
 	"context"
 	"path/filepath"
 	"testing"
+	"time"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
@@ -73,19 +74,11 @@ var _ = BeforeSuite(func() {
 	Expect(err).ToNot(HaveOccurred())
 	Expect(k8sClient).ToNot(BeNil())
 
-	err = (&Reconciler{
-		Client:                 k8sClient,
-		Scheme:                 k8sManager.GetScheme(),
-		Log:                    ctrl.Log.WithName("controllers").WithName("CustomResourceDefinition"),
-		SvcName:                "foo",
-		SvcNamespace:           "default",
-		SecretName:             "foo",
-		SecretNamespace:        "default",
-		CrdResources:           []string{"externalsecrets.test.io", "secretstores.test.io", "clustersecretstores.test.io"},
-		CAName:                 "external-secrets",
-		CAOrganization:         "external-secrets",
-		RestartOnSecretRefresh: false,
-	}).SetupWithManager(k8sManager, controller.Options{})
+	rec := New(k8sClient, k8sManager.GetScheme(), log, time.Second*1,
+		"foo", "default", "foo", "default", []string{
+			"secretstores.test.io",
+		})
+	rec.SetupWithManager(k8sManager, controller.Options{})
 	Expect(err).ToNot(HaveOccurred())
 
 	go func() {

+ 6 - 8
pkg/controllers/externalsecret/externalsecret_controller.go

@@ -36,11 +36,9 @@ import (
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
-	"github.com/external-secrets/external-secrets/pkg/provider"
 
 	// Loading registered providers.
 	_ "github.com/external-secrets/external-secrets/pkg/provider/register"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
@@ -135,7 +133,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 		return ctrl.Result{}, nil
 	}
 
-	storeProvider, err := schema.GetProvider(store)
+	storeProvider, err := esv1beta1.GetProvider(store)
 	if err != nil {
 		log.Error(err, errStoreProvider)
 		syncCallsError.With(syncCallsMetricLabels).Inc()
@@ -393,19 +391,19 @@ func (r *Reconciler) getStore(ctx context.Context, externalSecret *esv1beta1.Ext
 }
 
 // getProviderSecretData returns the provider's secret data with the provided ExternalSecret.
-func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient provider.SecretsClient, externalSecret *esv1beta1.ExternalSecret) (map[string][]byte, error) {
+func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient esv1beta1.SecretsClient, externalSecret *esv1beta1.ExternalSecret) (map[string][]byte, error) {
 	providerData := make(map[string][]byte)
 
 	for _, remoteRef := range externalSecret.Spec.DataFrom {
 		var secretMap map[string][]byte
 		var err error
-		if len(remoteRef.Find.Tags) > 0 || remoteRef.Find.Name != nil {
-			secretMap, err = providerClient.GetAllSecrets(ctx, remoteRef.Find)
+		if remoteRef.Find != nil {
+			secretMap, err = providerClient.GetAllSecrets(ctx, *remoteRef.Find)
 			if err != nil {
 				return nil, fmt.Errorf(errGetSecretKey, remoteRef.Extract.Key, externalSecret.Name, err)
 			}
-		} else if remoteRef.Extract.Key != "" {
-			secretMap, err = providerClient.GetSecretMap(ctx, remoteRef.Extract)
+		} else if remoteRef.Extract != nil {
+			secretMap, err = providerClient.GetSecretMap(ctx, *remoteRef.Extract)
 			if err != nil {
 				return nil, fmt.Errorf(errGetSecretKey, remoteRef.Extract.Key, externalSecret.Name, err)
 			}

+ 11 - 11
pkg/controllers/externalsecret/externalsecret_controller_test.go

@@ -30,8 +30,6 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 	"github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
 )
 
@@ -187,7 +185,9 @@ var _ = Describe("ExternalSecret controller", func() {
 				}
 				return true
 			},
-			checkExternalSecret: func(es *esv1beta1.ExternalSecret) {},
+			checkExternalSecret: func(es *esv1beta1.ExternalSecret) {
+				// noop by default
+			},
 			secretStore: &esv1beta1.SecretStore{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      ExternalSecretStore,
@@ -540,7 +540,7 @@ var _ = Describe("ExternalSecret controller", func() {
 		}
 		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
 			{
-				Extract: esv1beta1.ExternalSecretDataRemoteRef{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
 					Key: "datamap",
 				},
 			},
@@ -684,7 +684,7 @@ var _ = Describe("ExternalSecret controller", func() {
 		tc.externalSecret.Spec.Data = []esv1beta1.ExternalSecretData{}
 		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
 			{
-				Extract: esv1beta1.ExternalSecretDataRemoteRef{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
 					Key: remoteKey,
 				},
 			},
@@ -726,7 +726,7 @@ var _ = Describe("ExternalSecret controller", func() {
 		tc.externalSecret.Spec.Data = []esv1beta1.ExternalSecretData{}
 		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
 			{
-				Extract: esv1beta1.ExternalSecretDataRemoteRef{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
 					Key: remoteKey,
 				},
 			},
@@ -791,7 +791,7 @@ var _ = Describe("ExternalSecret controller", func() {
 		tc.externalSecret.Spec.Data = nil
 		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
 			{
-				Extract: esv1beta1.ExternalSecretDataRemoteRef{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
 					Key: remoteKey,
 				},
 			},
@@ -813,7 +813,7 @@ var _ = Describe("ExternalSecret controller", func() {
 		tc.externalSecret.Spec.Data = nil
 		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
 			{
-				Find: esv1beta1.ExternalSecretFind{
+				Find: &esv1beta1.ExternalSecretFind{
 					Name: &esv1beta1.FindName{
 						RegExp: "foobar",
 					},
@@ -844,7 +844,7 @@ var _ = Describe("ExternalSecret controller", func() {
 
 		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
 			{
-				Extract: esv1beta1.ExternalSecretDataRemoteRef{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
 					Key: remoteKey,
 				},
 			},
@@ -925,7 +925,7 @@ var _ = Describe("ExternalSecret controller", func() {
 	// a SecretSyncedError status condition must be set
 	storeConstructErrCondition := func(tc *testCase) {
 		fakeProvider.WithNew(func(context.Context, esv1beta1.GenericStore, client.Client,
-			string) (provider.SecretsClient, error) {
+			string) (esv1beta1.SecretsClient, error) {
 			return nil, fmt.Errorf("artificial constructor error")
 		})
 		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
@@ -1392,7 +1392,7 @@ func externalSecretConditionShouldBe(name, ns string, ct esv1beta1.ExternalSecre
 
 func init() {
 	fakeProvider = fake.New()
-	schema.ForceRegister(fakeProvider, &esv1beta1.SecretStoreProvider{
+	esv1beta1.ForceRegister(fakeProvider, &esv1beta1.SecretStoreProvider{
 		AWS: &esv1beta1.AWSProvider{
 			Service: esv1beta1.AWSServiceSecretsManager,
 		},

+ 1 - 2
pkg/controllers/secretstore/common.go

@@ -25,7 +25,6 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 )
 
 const (
@@ -77,7 +76,7 @@ func reconcile(ctx context.Context, req ctrl.Request, ss esapi.GenericStore, cl
 // if it fails sets a condition and writes events.
 func validateStore(ctx context.Context, namespace string, store esapi.GenericStore,
 	client client.Client, recorder record.EventRecorder) error {
-	storeProvider, err := schema.GetProvider(store)
+	storeProvider, err := esapi.GetProvider(store)
 	if err != nil {
 		cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionFalse, esapi.ReasonInvalidStore, errUnableGetProvider)
 		SetExternalSecretCondition(store, *cond)

+ 100 - 0
pkg/controllers/webhookconfig/suite_test.go

@@ -0,0 +1,100 @@
+/*
+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 webhookconfig
+
+import (
+	"context"
+	"path/filepath"
+	"testing"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/rest"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
+	"sigs.k8s.io/controller-runtime/pkg/envtest"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+var cfg *rest.Config
+var k8sClient client.Client
+var testEnv *envtest.Environment
+var cancel context.CancelFunc
+var reconciler *Reconciler
+
+const (
+	ctrlSvcName         = "foo"
+	ctrlSvcNamespace    = "default"
+	ctrlSecretName      = "foo"
+	ctrlSecretNamespace = "default"
+)
+
+func TestAPIs(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Controller Suite")
+}
+
+var _ = BeforeSuite(func() {
+	log := zap.New(zap.WriteTo(GinkgoWriter))
+	logf.SetLogger(log)
+
+	By("bootstrapping test environment")
+	testEnv = &envtest.Environment{
+		CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "deploy", "crds")},
+	}
+
+	var ctx context.Context
+	ctx, cancel = context.WithCancel(context.Background())
+
+	var err error
+	cfg, err = testEnv.Start()
+	Expect(err).ToNot(HaveOccurred())
+	Expect(cfg).ToNot(BeNil())
+
+	err = esapi.AddToScheme(scheme.Scheme)
+	Expect(err).NotTo(HaveOccurred())
+
+	k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
+		Scheme:             scheme.Scheme,
+		MetricsBindAddress: "0", // avoid port collision when testing
+	})
+	Expect(err).ToNot(HaveOccurred())
+
+	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
+	Expect(err).ToNot(HaveOccurred())
+	Expect(k8sClient).ToNot(BeNil())
+
+	reconciler = New(k8sClient, k8sManager.GetScheme(), ctrl.Log, ctrlSvcName, ctrlSvcNamespace, ctrlSecretName, ctrlSecretNamespace, time.Second)
+	reconciler.SetupWithManager(k8sManager, controller.Options{})
+	Expect(err).ToNot(HaveOccurred())
+
+	go func() {
+		defer GinkgoRecover()
+		Expect(k8sManager.Start(ctx)).ToNot(HaveOccurred())
+	}()
+})
+
+var _ = AfterSuite(func() {
+	By("tearing down the test environment")
+	cancel() // stop manager
+	err := testEnv.Stop()
+	Expect(err).ToNot(HaveOccurred())
+})

+ 186 - 0
pkg/controllers/webhookconfig/webhookconfig.go

@@ -0,0 +1,186 @@
+/*
+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 webhookconfig
+
+import (
+	"context"
+	"encoding/base64"
+	"fmt"
+	"net/http"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/go-logr/logr"
+	admissionregistration "k8s.io/api/admissionregistration/v1"
+	v1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/tools/record"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
+)
+
+type Reconciler struct {
+	client.Client
+	Log             logr.Logger
+	Scheme          *runtime.Scheme
+	recorder        record.EventRecorder
+	RequeueDuration time.Duration
+	SvcName         string
+	SvcNamespace    string
+	SecretName      string
+	SecretNamespace string
+
+	rdyMu *sync.Mutex
+	ready bool
+}
+
+func New(k8sClient client.Client, scheme *runtime.Scheme,
+	log logr.Logger, svcName, svcNamespace, secretName, secretNamespace string,
+	requeueInterval time.Duration) *Reconciler {
+	return &Reconciler{
+		Client:          k8sClient,
+		Scheme:          scheme,
+		Log:             log,
+		RequeueDuration: requeueInterval,
+		SvcName:         svcName,
+		SvcNamespace:    svcNamespace,
+		SecretName:      secretName,
+		SecretNamespace: secretNamespace,
+		rdyMu:           &sync.Mutex{},
+		ready:           false,
+	}
+}
+
+const (
+	wellKnownLabelKey   = "external-secrets.io/component"
+	wellKnownLabelValue = "webhook"
+
+	ReasonUpdateFailed   = "UpdateFailed"
+	errWebhookNotReady   = "webhook not ready"
+	errSubsetsNotReady   = "subsets not ready"
+	errAddressesNotReady = "addresses not ready"
+	errCACertNotReady    = "ca cert not yet ready"
+
+	caCertName = "ca.crt"
+)
+
+func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	log := r.Log.WithValues("Webhookconfig", req.NamespacedName)
+	var cfg admissionregistration.ValidatingWebhookConfiguration
+	err := r.Get(ctx, req.NamespacedName, &cfg)
+	if apierrors.IsNotFound(err) {
+		return ctrl.Result{}, nil
+	} else if err != nil {
+		log.Error(err, "unable to get Webhookconfig")
+		return ctrl.Result{}, err
+	}
+
+	if cfg.Labels[wellKnownLabelKey] != wellKnownLabelValue {
+		log.Info("ignoring webhook due to missing labels", wellKnownLabelKey, wellKnownLabelValue)
+		return ctrl.Result{}, nil
+	}
+
+	log.Info("updating webhook config")
+	err = r.updateConfig(ctx, &cfg)
+	if err != nil {
+		log.Error(err, "could not update webhook config")
+		r.recorder.Eventf(&cfg, v1.EventTypeWarning, ReasonUpdateFailed, err.Error())
+		return ctrl.Result{
+			RequeueAfter: time.Minute,
+		}, err
+	}
+	log.Info("updated webhook config")
+
+	// right now we only have one single
+	// webhook config we care about
+	r.rdyMu.Lock()
+	defer r.rdyMu.Unlock()
+	r.ready = true
+	return ctrl.Result{
+		RequeueAfter: r.RequeueDuration,
+	}, nil
+}
+
+func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
+	r.recorder = mgr.GetEventRecorderFor("validating-webhook-configuration")
+	return ctrl.NewControllerManagedBy(mgr).
+		WithOptions(opts).
+		For(&admissionregistration.ValidatingWebhookConfiguration{}).
+		Complete(r)
+}
+
+func (r *Reconciler) ReadyCheck(_ *http.Request) error {
+	r.rdyMu.Lock()
+	defer r.rdyMu.Unlock()
+	if !r.ready {
+		return fmt.Errorf(errWebhookNotReady)
+	}
+	var eps v1.Endpoints
+	err := r.Get(context.TODO(), types.NamespacedName{
+		Name:      r.SvcName,
+		Namespace: r.SvcNamespace,
+	}, &eps)
+	if err != nil {
+		return err
+	}
+	if len(eps.Subsets) == 0 {
+		return fmt.Errorf(errSubsetsNotReady)
+	}
+	if len(eps.Subsets[0].Addresses) == 0 {
+		return fmt.Errorf(errAddressesNotReady)
+	}
+	return nil
+}
+
+// reads the ca cert and updates the webhook config.
+func (r *Reconciler) updateConfig(ctx context.Context, cfg *admissionregistration.ValidatingWebhookConfiguration) error {
+	secret := v1.Secret{}
+	secretName := types.NamespacedName{
+		Name:      r.SecretName,
+		Namespace: r.SecretNamespace,
+	}
+	err := r.Get(context.Background(), secretName, &secret)
+	if err != nil {
+		return err
+	}
+
+	crt, ok := secret.Data[caCertName]
+	if !ok {
+		return fmt.Errorf(errCACertNotReady)
+	}
+	if err := r.inject(cfg, r.SvcName, r.SvcNamespace, crt); err != nil {
+		return err
+	}
+	return r.Update(ctx, cfg)
+}
+
+func (r *Reconciler) inject(cfg *admissionregistration.ValidatingWebhookConfiguration, svcName, svcNamespace string, certData []byte) error {
+	r.Log.Info("injecting ca certificate and service names", "cacrt", base64.StdEncoding.EncodeToString(certData), "name", cfg.Name)
+	for idx, w := range cfg.Webhooks {
+		if !strings.HasSuffix(w.Name, "external-secrets.io") {
+			r.Log.Info("skipping webhook", "name", cfg.Name, "webhook-name", w.Name)
+			continue
+		}
+		// we just patch the relevant fields
+		cfg.Webhooks[idx].ClientConfig.Service.Name = svcName
+		cfg.Webhooks[idx].ClientConfig.Service.Namespace = svcNamespace
+		cfg.Webhooks[idx].ClientConfig.CABundle = certData
+	}
+	return nil
+}

+ 324 - 0
pkg/controllers/webhookconfig/webhookconfig_test.go

@@ -0,0 +1,324 @@
+/*
+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 webhookconfig
+
+import (
+	"bytes"
+	"context"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	admissionregistration "k8s.io/api/admissionregistration/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/utils/pointer"
+)
+
+const defaultCACert = `-----BEGIN CERTIFICATE-----
+MIIDRjCCAi6gAwIBAgIBADANBgkqhkiG9w0BAQsFADA2MRkwFwYDVQQKExBleHRl
+cm5hbC1zZWNyZXRzMRkwFwYDVQQDExBleHRlcm5hbC1zZWNyZXRzMB4XDTIyMDIx
+NzEwMDYxMFoXDTMyMDIxNTExMDYxMFowNjEZMBcGA1UEChMQZXh0ZXJuYWwtc2Vj
+cmV0czEZMBcGA1UEAxMQZXh0ZXJuYWwtc2VjcmV0czCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAKSINgqU2dBdX8JpPjRHWSdpxuoltGl6xXmQHOhbTXAt
+/STDu7oi6eiFgepQ2QHuWLGwZgbbYnEhtLvw4dUwPcLyv6WIdeiUSA4pdFxL7asc
+WV4tjiRkRTJVrixJTxXpry/CsPqXBlvnu1YGESkrLOYCmA2xnDH8voEBbwYvXXB9
+3g5rOJncSh/7g+H55ZFFyWrIPyDUnfwE3CREjZXpsagFhRYpkuRlXTnU6t0OTEEh
+qLHlZ+ebUzL8NaegEgEHD32PrQPXpls1yurIrsA+I6McWkXGykykYHVK+1a1pL1g
+e+PBkegtwtX+EmB2ux7PVVeB4TTYqzCKbnObW4mJLZkCAwEAAaNfMF0wDgYDVR0P
+AQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHgSu/Im2gyu4TU0
+AWrMSFbtoVokMBsGA1UdEQQUMBKCEGV4dGVybmFsLXNlY3JldHMwDQYJKoZIhvcN
+AQELBQADggEBAJU88jCcPsAHN8DKLu+QMCoKYbeftX4gXxyoijGSde2w2O8NPtMP
+awu4Y5x3LNTwyIIxXi78UD0RI53GbUgHvS+X9v6CC2IZMS65xqKR+EsjzEh7Ldbm
+vZoF4ZDnfb2s5SK6MeYf67BE7XWpGfbHmjt6h80xsYjL6ovcik+dlu/AixMyLslS
+tDbMybAR8kR0zdQLYcZq7XEX5QsOO8qBn5rTfD6MiYik8ZrP7FqUMHyVpHiBuNio
+krnSOvynvuA9mlf2F2727dMt2Ij9uER+9QnhWBQex1h8CwALmm2k9G5Gt+RjB8oe
+lNjvmHAXUfOE/cbD7EP++X17kWt41FjmePc=
+-----END CERTIFICATE-----
+`
+
+type testCase struct {
+	vwc       *admissionregistration.ValidatingWebhookConfiguration
+	service   *corev1.Service
+	endpoints *corev1.Endpoints
+	secret    *corev1.Secret
+	assert    func()
+}
+
+var _ = Describe("ValidatingWebhookConfig reconcile", Ordered, func() {
+	var test *testCase
+
+	BeforeEach(func() {
+		test = makeDefaultTestcase()
+	})
+
+	AfterEach(func() {
+		ctx := context.Background()
+		k8sClient.Delete(ctx, test.vwc)
+		k8sClient.Delete(ctx, test.secret)
+		k8sClient.Delete(ctx, test.service)
+		k8sClient.Delete(ctx, test.endpoints)
+	})
+
+	// Should patch VWC
+	PatchAndReady := func(tc *testCase) {
+		tc.endpoints.Subsets = nil
+
+		// endpoints become ready in a moment
+		go func() {
+			<-time.After(time.Second * 4)
+			eps := makeEndpoints()
+			err := k8sClient.Update(context.Background(), eps)
+			Expect(err).ToNot(HaveOccurred())
+		}()
+		tc.assert = func() {
+			Eventually(func() bool {
+				// the controller should become ready at some point!
+				err := reconciler.ReadyCheck(nil)
+				return err == nil
+			}).
+				WithTimeout(time.Second * 10).
+				WithPolling(time.Second).
+				Should(BeTrue())
+
+			Eventually(func() bool {
+				var vwc admissionregistration.ValidatingWebhookConfiguration
+				err := k8sClient.Get(context.Background(), types.NamespacedName{
+					Name: tc.vwc.Name,
+				}, &vwc)
+				if err != nil {
+					return false
+				}
+				for _, wc := range vwc.Webhooks {
+					if !bytes.Equal(wc.ClientConfig.CABundle, []byte(defaultCACert)) {
+						return false
+					}
+					if wc.ClientConfig.Service == nil {
+						return false
+					}
+					if wc.ClientConfig.Service.Name != ctrlSvcName {
+						return false
+					}
+					if wc.ClientConfig.Service.Namespace != ctrlSvcNamespace {
+						return false
+					}
+				}
+				return true
+			}).
+				WithTimeout(time.Second * 10).
+				WithPolling(time.Second).
+				Should(BeTrue())
+		}
+	}
+
+	IgnoreNoMatch := func(tc *testCase) {
+		delete(tc.vwc.ObjectMeta.Labels, wellKnownLabelKey)
+		tc.assert = func() {
+			Consistently(func() bool {
+				var vwc admissionregistration.ValidatingWebhookConfiguration
+
+				err := k8sClient.Get(context.Background(), types.NamespacedName{
+					Name: tc.vwc.Name,
+				}, &vwc)
+				if err != nil {
+					return false
+				}
+				for _, wc := range vwc.Webhooks {
+					if bytes.Equal(wc.ClientConfig.CABundle, []byte(defaultCACert)) {
+						return false
+					}
+					if wc.ClientConfig.Service.Name == ctrlSvcName {
+						return false
+					}
+					if wc.ClientConfig.Service.Namespace == ctrlSvcNamespace {
+						return false
+					}
+				}
+				return true
+			}).
+				WithTimeout(time.Second * 10).
+				WithPolling(time.Second).
+				Should(BeTrue())
+		}
+	}
+
+	// Should patch and update VWC after requeue duration has passed
+	PatchAndUpdate := func(tc *testCase) {
+		foobar := "new value"
+		// ca cert will change after some time
+		go func() {
+			<-time.After(time.Second * 4)
+			sec := makeSecret()
+			sec.Data[caCertName] = []byte(foobar)
+			err := k8sClient.Update(context.Background(), sec)
+			Expect(err).ToNot(HaveOccurred())
+		}()
+		tc.assert = func() {
+
+			Eventually(func() bool {
+				var vwc admissionregistration.ValidatingWebhookConfiguration
+				err := k8sClient.Get(context.Background(), types.NamespacedName{
+					Name: tc.vwc.Name,
+				}, &vwc)
+				if err != nil {
+					return false
+				}
+				for _, wc := range vwc.Webhooks {
+					if !bytes.Equal(wc.ClientConfig.CABundle, []byte(foobar)) {
+						return false
+					}
+					if wc.ClientConfig.Service == nil {
+						return false
+					}
+					if wc.ClientConfig.Service.Name != ctrlSvcName {
+						return false
+					}
+					if wc.ClientConfig.Service.Namespace != ctrlSvcNamespace {
+						return false
+					}
+				}
+				return true
+			}).
+				WithTimeout(time.Second * 10).
+				WithPolling(time.Second).
+				Should(BeTrue())
+		}
+	}
+
+	DescribeTable("Controller Reconcile logic", func(muts ...func(tc *testCase)) {
+		for _, mut := range muts {
+			mut(test)
+		}
+		ctx := context.Background()
+
+		err := k8sClient.Create(ctx, test.vwc)
+		Expect(err).ToNot(HaveOccurred())
+
+		err = k8sClient.Create(ctx, test.secret)
+		Expect(err).ToNot(HaveOccurred())
+
+		err = k8sClient.Create(ctx, test.service)
+		Expect(err).ToNot(HaveOccurred())
+
+		err = k8sClient.Create(ctx, test.endpoints)
+		Expect(err).ToNot(HaveOccurred())
+
+		test.assert()
+	},
+
+		Entry("should patch matching webhook configs", PatchAndReady),
+		Entry("should update vwc with new ca cert after requeue duration", PatchAndUpdate),
+		Entry("should ignore when vwc labels are missing", IgnoreNoMatch),
+	)
+
+})
+
+func makeValidatingWebhookConfig() *admissionregistration.ValidatingWebhookConfiguration {
+	return &admissionregistration.ValidatingWebhookConfiguration{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "name-shouldnt-matter",
+			Labels: map[string]string{
+				wellKnownLabelKey: wellKnownLabelValue,
+			},
+		},
+		Webhooks: []admissionregistration.ValidatingWebhook{
+			{
+				Name:                    "secretstores.external-secrets.io",
+				SideEffects:             (*admissionregistration.SideEffectClass)(pointer.StringPtr(string(admissionregistration.SideEffectClassNone))),
+				AdmissionReviewVersions: []string{"v1"},
+				ClientConfig: admissionregistration.WebhookClientConfig{
+					CABundle: []byte("Cg=="),
+					Service: &admissionregistration.ServiceReference{
+						Name:      "noop",
+						Namespace: "noop",
+						Path:      pointer.StringPtr("/validate-secretstore"),
+					},
+				},
+			},
+			{
+				Name:                    "clustersecretstores.external-secrets.io",
+				SideEffects:             (*admissionregistration.SideEffectClass)(pointer.StringPtr(string(admissionregistration.SideEffectClassNone))),
+				AdmissionReviewVersions: []string{"v1"},
+				ClientConfig: admissionregistration.WebhookClientConfig{
+					CABundle: []byte("Cg=="),
+					Service: &admissionregistration.ServiceReference{
+						Name:      "noop",
+						Namespace: "noop",
+						Path:      pointer.StringPtr("/validate-clustersecretstore"),
+					},
+				},
+			},
+		},
+	}
+}
+
+func makeSecret() *corev1.Secret {
+	return &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      ctrlSecretName,
+			Namespace: ctrlSecretNamespace,
+		},
+		Data: map[string][]byte{
+			caCertName: []byte(defaultCACert),
+		},
+	}
+}
+
+func makeService() *corev1.Service {
+	return &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      ctrlSvcName,
+			Namespace: ctrlSvcNamespace,
+		},
+		Spec: corev1.ServiceSpec{
+			Ports: []corev1.ServicePort{
+				{
+					Name: "http",
+					Port: 80,
+				},
+			},
+		},
+	}
+}
+
+func makeEndpoints() *corev1.Endpoints {
+	return &corev1.Endpoints{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      ctrlSvcName,
+			Namespace: ctrlSvcNamespace,
+		},
+		Subsets: []corev1.EndpointSubset{
+			{
+				Addresses: []corev1.EndpointAddress{
+					{
+						IP: "1.2.3.4",
+					},
+				},
+			},
+		},
+	}
+}
+
+func makeDefaultTestcase() *testCase {
+	return &testCase{
+		assert: func() {
+			// this is a noop by default
+		},
+		vwc:       makeValidatingWebhookConfig(),
+		secret:    makeSecret(),
+		service:   makeService(),
+		endpoints: makeEndpoints(),
+	}
+}

+ 7 - 5
pkg/provider/akeyless/akeyless.go

@@ -24,8 +24,6 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
@@ -56,17 +54,21 @@ type akeylessVaultInterface interface {
 }
 
 func init() {
-	schema.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+	esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
 		Akeyless: &esv1beta1.AkeylessProvider{},
 	})
 }
 
 // NewClient constructs a new secrets client based on the provided store.
-func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
+func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	return newClient(ctx, store, kube, namespace)
 }
 
-func newClient(_ context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
+	return nil
+}
+
+func newClient(_ context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	akl := &akeylessBase{
 		kube:      kube,
 		store:     store,

+ 6 - 4
pkg/provider/alibaba/kms.go

@@ -26,9 +26,7 @@ import (
 	kclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
 	"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
@@ -165,7 +163,7 @@ func (kms *KeyManagementService) GetSecretMap(ctx context.Context, ref esv1beta1
 }
 
 // NewClient constructs a new secrets client based on the provided store.
-func (kms *KeyManagementService) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
+func (kms *KeyManagementService) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	storeSpec := store.GetSpec()
 	alibabaSpec := storeSpec.Provider.Alibaba
 	iStore := &Client{
@@ -196,8 +194,12 @@ func (kms *KeyManagementService) Validate() error {
 	return nil
 }
 
+func (kms *KeyManagementService) ValidateStore(store esv1beta1.GenericStore) error {
+	return nil
+}
+
 func init() {
-	schema.Register(&KeyManagementService{}, &esv1beta1.SecretStoreProvider{
+	esv1beta1.Register(&KeyManagementService{}, &esv1beta1.SecretStoreProvider{
 		Alibaba: &esv1beta1.AlibabaProvider{},
 	})
 }

+ 53 - 5
pkg/provider/aws/provider.go

@@ -18,15 +18,15 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/aws/aws-sdk-go/aws/endpoints"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
 	awsauth "github.com/external-secrets/external-secrets/pkg/provider/aws/auth"
 	"github.com/external-secrets/external-secrets/pkg/provider/aws/parameterstore"
 	"github.com/external-secrets/external-secrets/pkg/provider/aws/secretsmanager"
 	"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
+	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
 // Provider satisfies the provider interface.
@@ -35,14 +35,62 @@ type Provider struct{}
 const (
 	errUnableCreateSession    = "unable to create session: %w"
 	errUnknownProviderService = "unknown AWS Provider Service: %s"
+	errRegionNotFound         = "region not found: %s"
 )
 
 // NewClient constructs a new secrets client based on the provided store.
-func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
+func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	return newClient(ctx, store, kube, namespace, awsauth.DefaultSTSProvider)
 }
 
-func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, assumeRoler awsauth.STSProvider) (provider.SecretsClient, error) {
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
+	prov, err := util.GetAWSProvider(store)
+	if err != nil {
+		return err
+	}
+	err = validateRegion(prov)
+	if err != nil {
+		return err
+	}
+
+	// case: static credentials
+	if prov.Auth.SecretRef != nil {
+		if err := utils.ValidateSecretSelector(store, prov.Auth.SecretRef.AccessKeyID); err != nil {
+			return fmt.Errorf("invalid Auth.SecretRef.AccessKeyID: %w", err)
+		}
+		if err := utils.ValidateSecretSelector(store, prov.Auth.SecretRef.SecretAccessKey); err != nil {
+			return fmt.Errorf("invalid Auth.SecretRef.SecretAccessKey: %w", err)
+		}
+	}
+
+	// case: jwt credentials
+	if prov.Auth.JWTAuth != nil && prov.Auth.JWTAuth.ServiceAccountRef != nil {
+		if err := utils.ValidateServiceAccountSelector(store, *prov.Auth.JWTAuth.ServiceAccountRef); err != nil {
+			return fmt.Errorf("invalid Auth.JWT.ServiceAccountRef: %w", err)
+		}
+	}
+
+	return nil
+}
+
+func validateRegion(prov *esv1beta1.AWSProvider) error {
+	resolver := endpoints.DefaultResolver()
+	partitions := resolver.(endpoints.EnumPartitions).Partitions()
+	found := false
+	for _, p := range partitions {
+		for id := range p.Regions() {
+			if id == prov.Region {
+				found = true
+			}
+		}
+	}
+	if !found {
+		return fmt.Errorf(errRegionNotFound, prov.Region)
+	}
+	return nil
+}
+
+func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, assumeRoler awsauth.STSProvider) (esv1beta1.SecretsClient, error) {
 	prov, err := util.GetAWSProvider(store)
 	if err != nil {
 		return nil, err
@@ -63,7 +111,7 @@ func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Cl
 }
 
 func init() {
-	schema.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+	esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
 		AWS: &esv1beta1.AWSProvider{},
 	})
 }

+ 197 - 0
pkg/provider/aws/provider_test.go

@@ -21,6 +21,8 @@ import (
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/stretchr/testify/assert"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/pointer"
 	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
@@ -146,3 +148,198 @@ func TestProvider(t *testing.T) {
 		})
 	}
 }
+
+const validRegion = "eu-central-1"
+
+func TestValidateStore(t *testing.T) {
+	type args struct {
+		store esv1beta1.GenericStore
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name:    "invalid region",
+			wantErr: true,
+			args: args{
+				store: &esv1beta1.SecretStore{
+					Spec: esv1beta1.SecretStoreSpec{
+						Provider: &esv1beta1.SecretStoreProvider{
+							AWS: &esv1beta1.AWSProvider{
+								Region: "noop.",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "valid region",
+			args: args{
+				store: &esv1beta1.SecretStore{
+					Spec: esv1beta1.SecretStoreSpec{
+						Provider: &esv1beta1.SecretStoreProvider{
+							AWS: &esv1beta1.AWSProvider{
+								Region: validRegion,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid static creds auth / AccessKeyID",
+			wantErr: true,
+			args: args{
+				store: &esv1beta1.SecretStore{
+					Spec: esv1beta1.SecretStoreSpec{
+						Provider: &esv1beta1.SecretStoreProvider{
+							AWS: &esv1beta1.AWSProvider{
+								Region: validRegion,
+								Auth: esv1beta1.AWSAuth{
+									SecretRef: &esv1beta1.AWSAuthSecretRef{
+										AccessKeyID: esmeta.SecretKeySelector{
+											Name:      "foobar",
+											Namespace: pointer.StringPtr("unacceptable"),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid static creds auth / SecretAccessKey",
+			wantErr: true,
+			args: args{
+				store: &esv1beta1.SecretStore{
+					Spec: esv1beta1.SecretStoreSpec{
+						Provider: &esv1beta1.SecretStoreProvider{
+							AWS: &esv1beta1.AWSProvider{
+								Region: validRegion,
+								Auth: esv1beta1.AWSAuth{
+									SecretRef: &esv1beta1.AWSAuthSecretRef{
+										SecretAccessKey: esmeta.SecretKeySelector{
+											Name:      "foobar",
+											Namespace: pointer.StringPtr("unacceptable"),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid static creds auth / SecretAccessKey missing namespace",
+			wantErr: true,
+			args: args{
+				store: &esv1beta1.ClusterSecretStore{
+					TypeMeta: v1.TypeMeta{
+						Kind: esv1beta1.ClusterSecretStoreKind,
+					},
+					Spec: esv1beta1.SecretStoreSpec{
+						Provider: &esv1beta1.SecretStoreProvider{
+							AWS: &esv1beta1.AWSProvider{
+								Region: validRegion,
+								Auth: esv1beta1.AWSAuth{
+									SecretRef: &esv1beta1.AWSAuthSecretRef{
+										SecretAccessKey: esmeta.SecretKeySelector{
+											Name: "foobar",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid static creds auth / AccessKeyID missing namespace",
+			wantErr: true,
+			args: args{
+				store: &esv1beta1.ClusterSecretStore{
+					TypeMeta: v1.TypeMeta{
+						Kind: esv1beta1.ClusterSecretStoreKind,
+					},
+					Spec: esv1beta1.SecretStoreSpec{
+						Provider: &esv1beta1.SecretStoreProvider{
+							AWS: &esv1beta1.AWSProvider{
+								Region: validRegion,
+								Auth: esv1beta1.AWSAuth{
+									SecretRef: &esv1beta1.AWSAuthSecretRef{
+										AccessKeyID: esmeta.SecretKeySelector{
+											Name: "foobar",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid jwt auth: missing sa selector namespace",
+			wantErr: true,
+			args: args{
+				store: &esv1beta1.ClusterSecretStore{
+					TypeMeta: v1.TypeMeta{
+						Kind: esv1beta1.ClusterSecretStoreKind,
+					},
+					Spec: esv1beta1.SecretStoreSpec{
+						Provider: &esv1beta1.SecretStoreProvider{
+							AWS: &esv1beta1.AWSProvider{
+								Region: validRegion,
+								Auth: esv1beta1.AWSAuth{
+									JWTAuth: &esv1beta1.AWSJWTAuth{
+										ServiceAccountRef: &esmeta.ServiceAccountSelector{
+											Name: "foobar",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid jwt auth: not allowed sa selector namespace",
+			wantErr: true,
+			args: args{
+				store: &esv1beta1.SecretStore{
+					Spec: esv1beta1.SecretStoreSpec{
+						Provider: &esv1beta1.SecretStoreProvider{
+							AWS: &esv1beta1.AWSProvider{
+								Region: validRegion,
+								Auth: esv1beta1.AWSAuth{
+									JWTAuth: &esv1beta1.AWSJWTAuth{
+										ServiceAccountRef: &esmeta.ServiceAccountSelector{
+											Name:      "foobar",
+											Namespace: pointer.StringPtr("unacceptable"),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p := &Provider{}
+			if err := p.ValidateStore(tt.args.store); (err != nil) != tt.wantErr {
+				t.Errorf("Provider.ValidateStore() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}

+ 4 - 0
pkg/provider/aws/secretsmanager/fake/fake.go

@@ -42,6 +42,10 @@ func (sm *Client) GetSecretValue(in *awssm.GetSecretValueInput) (*awssm.GetSecre
 	return nil, fmt.Errorf("test case not found")
 }
 
+func (sm *Client) ListSecrets(*awssm.ListSecretsInput) (*awssm.ListSecretsOutput, error) {
+	return nil, nil
+}
+
 func (sm *Client) cacheKeyForInput(in *awssm.GetSecretValueInput) string {
 	var secretID, versionID string
 	if in.SecretId != nil {

+ 41 - 5
pkg/provider/azure/keyvault/keyvault.go

@@ -30,8 +30,7 @@ import (
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
+	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
 const (
@@ -52,6 +51,13 @@ const (
 	errMissingClientIDSecret = "missing accessKeyID/secretAccessKey in store config"
 	errFindSecret            = "could not find secret %s/%s: %w"
 	errFindDataKey           = "no data for %q in secret '%s/%s'"
+
+	errInvalidStore              = "invalid store"
+	errInvalidStoreSpec          = "invalid store spec"
+	errInvalidStoreProv          = "invalid store provider"
+	errInvalidAzureProv          = "invalid azure keyvault provider"
+	errInvalidSecRefClientID     = "invalid AuthSecretRef.ClientID: %w"
+	errInvalidSecRefClientSecret = "invalid AuthSecretRef.ClientSecret: %w"
 )
 
 // interface to keyvault.BaseClient.
@@ -71,17 +77,17 @@ type Azure struct {
 }
 
 func init() {
-	schema.Register(&Azure{}, &esv1beta1.SecretStoreProvider{
+	esv1beta1.Register(&Azure{}, &esv1beta1.SecretStoreProvider{
 		AzureKV: &esv1beta1.AzureKVProvider{},
 	})
 }
 
 // NewClient constructs a new secrets client based on the provided store.
-func (a *Azure) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
+func (a *Azure) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	return newClient(ctx, store, kube, namespace)
 }
 
-func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
+func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	provider, err := getProvider(store)
 	if err != nil {
 		return nil, err
@@ -115,6 +121,36 @@ func getProvider(store esv1beta1.GenericStore) (*esv1beta1.AzureKVProvider, erro
 	return spc.Provider.AzureKV, nil
 }
 
+func (a *Azure) ValidateStore(store esv1beta1.GenericStore) error {
+	if store == nil {
+		return fmt.Errorf(errInvalidStore)
+	}
+	spc := store.GetSpec()
+	if spc == nil {
+		return fmt.Errorf(errInvalidStoreSpec)
+	}
+	if spc.Provider == nil {
+		return fmt.Errorf(errInvalidStoreProv)
+	}
+	p := spc.Provider.AzureKV
+	if p == nil {
+		return fmt.Errorf(errInvalidAzureProv)
+	}
+	if p.AuthSecretRef != nil {
+		if p.AuthSecretRef.ClientID != nil {
+			if err := utils.ValidateSecretSelector(store, *p.AuthSecretRef.ClientID); err != nil {
+				return fmt.Errorf(errInvalidSecRefClientID, err)
+			}
+		}
+		if p.AuthSecretRef.ClientSecret != nil {
+			if err := utils.ValidateSecretSelector(store, *p.AuthSecretRef.ClientSecret); err != nil {
+				return fmt.Errorf(errInvalidSecRefClientSecret, err)
+			}
+		}
+	}
+	return nil
+}
+
 // Empty GetAllSecrets.
 func (a *Azure) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
 	// TO be implemented

+ 64 - 3
pkg/provider/azure/keyvault/keyvault_test.go

@@ -30,7 +30,6 @@ import (
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
 	fake "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault/fake"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 	utils "github.com/external-secrets/external-secrets/pkg/utils"
 )
 
@@ -99,7 +98,7 @@ func TestNewClientManagedIdentityNoNeedForCredentials(t *testing.T) {
 		}}},
 	}
 
-	provider, err := schema.GetProvider(&store)
+	provider, err := esv1beta1.GetProvider(&store)
 	tassert.Nil(t, err, "the return err should be nil")
 	k8sClient := clientfake.NewClientBuilder().Build()
 	secretClient, err := provider.NewClient(context.Background(), &store, k8sClient, namespace)
@@ -127,7 +126,7 @@ func TestNewClientNoCreds(t *testing.T) {
 			TenantID: &tenantID,
 		}}},
 	}
-	provider, err := schema.GetProvider(&store)
+	provider, err := esv1beta1.GetProvider(&store)
 	tassert.Nil(t, err, "the return err should be nil")
 	k8sClient := clientfake.NewClientBuilder().Build()
 	_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
@@ -381,3 +380,65 @@ func makeValidRef() *esv1beta1.ExternalSecretDataRemoteRef {
 		Version: "default",
 	}
 }
+
+func TestValidateStore(t *testing.T) {
+	type args struct {
+		auth esv1beta1.AzureKVAuth
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name:    "empty auth",
+			wantErr: false,
+		},
+		{
+			name:    "empty client id",
+			wantErr: false,
+			args: args{
+				auth: esv1beta1.AzureKVAuth{},
+			},
+		},
+		{
+			name:    "invalid client id",
+			wantErr: true,
+			args: args{
+				auth: esv1beta1.AzureKVAuth{
+					ClientID: &v1.SecretKeySelector{
+						Namespace: pointer.StringPtr("invalid"),
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid client secret",
+			wantErr: true,
+			args: args{
+				auth: esv1beta1.AzureKVAuth{
+					ClientSecret: &v1.SecretKeySelector{
+						Namespace: pointer.StringPtr("invalid"),
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			a := &Azure{}
+			store := &esv1beta1.SecretStore{
+				Spec: esv1beta1.SecretStoreSpec{
+					Provider: &esv1beta1.SecretStoreProvider{
+						AzureKV: &esv1beta1.AzureKVProvider{
+							AuthSecretRef: &tt.args.auth,
+						},
+					},
+				},
+			}
+			if err := a.ValidateStore(store); (err != nil) != tt.wantErr {
+				t.Errorf("Azure.ValidateStore() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}

+ 6 - 4
pkg/provider/fake/fake.go

@@ -21,8 +21,6 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 )
 
 var (
@@ -35,7 +33,7 @@ type Provider struct {
 	config *esv1beta1.FakeProvider
 }
 
-func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
+func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	cfg, err := getProvider(store)
 	if err != nil {
 		return nil, err
@@ -99,8 +97,12 @@ func (p *Provider) Validate() error {
 	return nil
 }
 
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
+	return nil
+}
+
 func init() {
-	schema.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+	esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
 		Fake: &esv1beta1.FakeProvider{},
 	})
 }

+ 37 - 4
pkg/provider/gcp/secretmanager/secretsmanager.go

@@ -30,8 +30,6 @@ import (
 	kclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
@@ -52,6 +50,13 @@ const (
 	errUninitalizedGCPProvider                = "provider GCP is not initialized"
 	errClientGetSecretAccess                  = "unable to access Secret from SecretManager Client: %w"
 	errJSONSecretUnmarshal                    = "unable to unmarshal secret: %w"
+
+	errInvalidStore         = "invalid store"
+	errInvalidStoreSpec     = "invalid store spec"
+	errInvalidStoreProv     = "invalid store provider"
+	errInvalidGCPProv       = "invalid gcp secrets manager provider"
+	errInvalidAuthSecretRef = "invalid auth secret ref: %w"
+	errInvalidWISARef       = "invalid workload identity service account reference: %w"
 )
 
 type GoogleSecretManagerClient interface {
@@ -132,7 +137,7 @@ func serviceAccountTokenSource(ctx context.Context, store esv1beta1.GenericStore
 }
 
 // NewClient constructs a GCP Provider.
-func (sm *ProviderGCP) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
+func (sm *ProviderGCP) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	storeSpec := store.GetSpec()
 	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.GCPSM == nil {
 		return nil, fmt.Errorf(errGCPSMStore)
@@ -270,8 +275,36 @@ func (sm *ProviderGCP) Validate() error {
 	return nil
 }
 
+func (sm *ProviderGCP) ValidateStore(store esv1beta1.GenericStore) error {
+	if store == nil {
+		return fmt.Errorf(errInvalidStore)
+	}
+	spc := store.GetSpec()
+	if spc == nil {
+		return fmt.Errorf(errInvalidStoreSpec)
+	}
+	if spc.Provider == nil {
+		return fmt.Errorf(errInvalidStoreProv)
+	}
+	p := spc.Provider.GCPSM
+	if p == nil {
+		return fmt.Errorf(errInvalidGCPProv)
+	}
+	if p.Auth.SecretRef != nil {
+		if err := utils.ValidateSecretSelector(store, p.Auth.SecretRef.SecretAccessKey); err != nil {
+			return fmt.Errorf(errInvalidAuthSecretRef, err)
+		}
+	}
+	if p.Auth.WorkloadIdentity != nil {
+		if err := utils.ValidateServiceAccountSelector(store, p.Auth.WorkloadIdentity.ServiceAccountRef); err != nil {
+			return fmt.Errorf(errInvalidWISARef, err)
+		}
+	}
+	return nil
+}
+
 func init() {
-	schema.Register(&ProviderGCP{}, &esv1beta1.SecretStoreProvider{
+	esv1beta1.Register(&ProviderGCP{}, &esv1beta1.SecretStoreProvider{
 		GCPSM: &esv1beta1.GCPSMProvider{},
 	})
 }

+ 64 - 0
pkg/provider/gcp/secretmanager/secretsmanager_test.go

@@ -21,8 +21,10 @@ import (
 	"testing"
 
 	secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
+	"k8s.io/utils/pointer"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
 	fakesm "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager/fake"
 )
 
@@ -210,3 +212,65 @@ func ErrorContains(out error, want string) bool {
 	}
 	return strings.Contains(out.Error(), want)
 }
+
+func TestValidateStore(t *testing.T) {
+	type args struct {
+		auth esv1beta1.GCPSMAuth
+	}
+
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name:    "empty auth",
+			wantErr: false,
+		},
+		{
+			name:    "invalid secret ref",
+			wantErr: true,
+			args: args{
+				auth: esv1beta1.GCPSMAuth{
+					SecretRef: &esv1beta1.GCPSMAuthSecretRef{
+						SecretAccessKey: v1.SecretKeySelector{
+							Name:      "foo",
+							Namespace: pointer.StringPtr("invalid"),
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid wi sa ref",
+			wantErr: true,
+			args: args{
+				auth: esv1beta1.GCPSMAuth{
+					WorkloadIdentity: &esv1beta1.GCPWorkloadIdentity{
+						ServiceAccountRef: v1.ServiceAccountSelector{
+							Name:      "foo",
+							Namespace: pointer.StringPtr("invalid"),
+						},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			sm := &ProviderGCP{}
+			store := &esv1beta1.SecretStore{
+				Spec: esv1beta1.SecretStoreSpec{
+					Provider: &esv1beta1.SecretStoreProvider{
+						GCPSM: &esv1beta1.GCPSMProvider{
+							Auth: tt.args.auth,
+						},
+					},
+				},
+			}
+			if err := sm.ValidateStore(store); (err != nil) != tt.wantErr {
+				t.Errorf("ProviderGCP.ValidateStore() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}

+ 6 - 4
pkg/provider/gitlab/gitlab.go

@@ -27,8 +27,6 @@ import (
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	"github.com/external-secrets/external-secrets/e2e/framework/log"
-	"github.com/external-secrets/external-secrets/pkg/provider"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
@@ -63,7 +61,7 @@ type gClient struct {
 }
 
 func init() {
-	schema.Register(&Gitlab{}, &esv1beta1.SecretStoreProvider{
+	esv1beta1.Register(&Gitlab{}, &esv1beta1.SecretStoreProvider{
 		Gitlab: &esv1beta1.GitlabProvider{},
 	})
 }
@@ -108,7 +106,7 @@ func NewGitlabProvider() *Gitlab {
 }
 
 // Method on Gitlab Provider to set up client with credentials and populate projectID.
-func (g *Gitlab) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
+func (g *Gitlab) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	storeSpec := store.GetSpec()
 	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Gitlab == nil {
 		return nil, fmt.Errorf("no store type or wrong store type")
@@ -220,3 +218,7 @@ func (g *Gitlab) Close(ctx context.Context) error {
 func (g *Gitlab) Validate() error {
 	return nil
 }
+
+func (g *Gitlab) ValidateStore(store esv1beta1.GenericStore) error {
+	return nil
+}

+ 6 - 4
pkg/provider/ibm/provider.go

@@ -27,8 +27,6 @@ import (
 	kclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
@@ -323,7 +321,11 @@ func (ibm *providerIBM) Validate() error {
 	return nil
 }
 
-func (ibm *providerIBM) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
+func (ibm *providerIBM) ValidateStore(store esv1beta1.GenericStore) error {
+	return nil
+}
+
+func (ibm *providerIBM) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	storeSpec := store.GetSpec()
 	ibmSpec := storeSpec.Provider.IBM
 
@@ -376,7 +378,7 @@ func (ibm *providerIBM) NewClient(ctx context.Context, store esv1beta1.GenericSt
 }
 
 func init() {
-	schema.Register(&providerIBM{}, &esv1beta1.SecretStoreProvider{
+	esv1beta1.Register(&providerIBM{}, &esv1beta1.SecretStoreProvider{
 		IBM: &esv1beta1.IBMProvider{},
 	})
 }

+ 7 - 5
pkg/provider/kubernetes/kubernetes.go

@@ -27,8 +27,6 @@ import (
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
@@ -51,7 +49,7 @@ type ProviderKubernetes struct {
 	Client KClient
 }
 
-var _ provider.SecretsClient = &ProviderKubernetes{}
+var _ esv1beta1.SecretsClient = &ProviderKubernetes{}
 
 type BaseClient struct {
 	kube        kclient.Client
@@ -65,13 +63,13 @@ type BaseClient struct {
 }
 
 func init() {
-	schema.Register(&ProviderKubernetes{}, &esv1beta1.SecretStoreProvider{
+	esv1beta1.Register(&ProviderKubernetes{}, &esv1beta1.SecretStoreProvider{
 		Kubernetes: &esv1beta1.KubernetesProvider{},
 	})
 }
 
 // NewClient constructs a Kubernetes Provider.
-func (k *ProviderKubernetes) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
+func (k *ProviderKubernetes) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	storeSpec := store.GetSpec()
 	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Kubernetes == nil {
 		return nil, fmt.Errorf("no store type or wrong store type")
@@ -233,3 +231,7 @@ func (k *BaseClient) fetchSecretKey(ctx context.Context, key esmeta.SecretKeySel
 func (k *ProviderKubernetes) Validate() error {
 	return nil
 }
+
+func (k *ProviderKubernetes) ValidateStore(store esv1beta1.GenericStore) error {
+	return nil
+}

+ 6 - 4
pkg/provider/oracle/oracle.go

@@ -27,9 +27,7 @@ import (
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
 	"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
@@ -124,7 +122,7 @@ func (vms *VaultManagementService) GetSecretMap(ctx context.Context, ref esv1bet
 }
 
 // NewClient constructs a new secrets client based on the provided store.
-func (vms *VaultManagementService) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
+func (vms *VaultManagementService) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	storeSpec := store.GetSpec()
 	oracleSpec := storeSpec.Provider.Oracle
 
@@ -225,8 +223,12 @@ func (vms *VaultManagementService) Validate() error {
 	return nil
 }
 
+func (vms *VaultManagementService) ValidateStore(store esv1beta1.GenericStore) error {
+	return nil
+}
+
 func init() {
-	schema.Register(&VaultManagementService{}, &esv1beta1.SecretStoreProvider{
+	esv1beta1.Register(&VaultManagementService{}, &esv1beta1.SecretStoreProvider{
 		Oracle: &esv1beta1.OracleProvider{},
 	})
 }

+ 10 - 8
pkg/provider/testing/fake/fake.go

@@ -20,16 +20,14 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 )
 
-var _ provider.Provider = &Client{}
+var _ esv1beta1.Provider = &Client{}
 
 // Client is a fake client for testing.
 type Client struct {
 	NewFn func(context.Context, esv1beta1.GenericStore, client.Client,
-		string) (provider.SecretsClient, error)
+		string) (esv1beta1.SecretsClient, error)
 	GetSecretFn     func(context.Context, esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error)
 	GetSecretMapFn  func(context.Context, esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
 	GetAllSecretsFn func(context.Context, esv1beta1.ExternalSecretFind) (map[string][]byte, error)
@@ -49,7 +47,7 @@ func New() *Client {
 		},
 	}
 
-	v.NewFn = func(context.Context, esv1beta1.GenericStore, client.Client, string) (provider.SecretsClient, error) {
+	v.NewFn = func(context.Context, esv1beta1.GenericStore, client.Client, string) (esv1beta1.SecretsClient, error) {
 		return v, nil
 	}
 
@@ -58,7 +56,7 @@ func New() *Client {
 
 // RegisterAs registers the fake client in the schema.
 func (v *Client) RegisterAs(provider *esv1beta1.SecretStoreProvider) {
-	schema.ForceRegister(v, provider)
+	esv1beta1.ForceRegister(v, provider)
 }
 
 // GetSecret implements the provider.Provider interface.
@@ -92,6 +90,10 @@ func (v *Client) Validate() error {
 	return nil
 }
 
+func (v *Client) ValidateStore(store esv1beta1.GenericStore) error {
+	return nil
+}
+
 // WithGetSecretMap wraps the secret data map returned by this fake provider.
 func (v *Client) WithGetSecretMap(secData map[string][]byte, err error) *Client {
 	v.GetSecretMapFn = func(context.Context, esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
@@ -110,13 +112,13 @@ func (v *Client) WithGetAllSecrets(secData map[string][]byte, err error) *Client
 
 // WithNew wraps the fake provider factory function.
 func (v *Client) WithNew(f func(context.Context, esv1beta1.GenericStore, client.Client,
-	string) (provider.SecretsClient, error)) *Client {
+	string) (esv1beta1.SecretsClient, error)) *Client {
 	v.NewFn = f
 	return v
 }
 
 // NewClient returns a new fake provider.
-func (v *Client) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
+func (v *Client) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	c, err := v.NewFn(ctx, store, kube, namespace)
 	if err != nil {
 		return nil, err

+ 76 - 6
pkg/provider/vault/vault.go

@@ -37,13 +37,12 @@ import (
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
+	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
 var (
-	_ provider.Provider      = &connector{}
-	_ provider.SecretsClient = &client{}
+	_ esv1beta1.Provider      = &connector{}
+	_ esv1beta1.SecretsClient = &client{}
 )
 
 const (
@@ -79,6 +78,19 @@ const (
 
 	errUnknownCAProvider = "unknown caProvider type given"
 	errCANamespace       = "cannot read secret for CAProvider due to missing namespace on kind ClusterSecretStore"
+
+	errInvalidStore      = "invalid store"
+	errInvalidStoreSpec  = "invalid store spec"
+	errInvalidStoreProv  = "invalid store provider"
+	errInvalidVaultProv  = "invalid vault provider"
+	errInvalidAppRoleSec = "invalid Auth.AppRole.SecretRef: %w"
+	errInvalidClientCert = "invalid Auth.Cert.ClientCert: %w"
+	errInvalidCertSec    = "invalid Auth.Cert.SecretRef: %w"
+	errInvalidJwtSec     = "invalid Auth.Jwt.SecretRef: %w"
+	errInvalidKubeSA     = "invalid Auth.Kubernetes.ServiceAccountRef: %w"
+	errInvalidKubeSec    = "invalid Auth.Kubernetes.SecretRef: %w"
+	errInvalidLdapSec    = "invalid Auth.Ldap.SecretRef: %w"
+	errInvalidTokenRef   = "invalid Auth.TokenSecretRef: %w"
 )
 
 type Client interface {
@@ -101,7 +113,7 @@ type client struct {
 }
 
 func init() {
-	schema.Register(&connector{
+	esv1beta1.Register(&connector{
 		newVaultClient: newVaultClient,
 	}, &esv1beta1.SecretStoreProvider{
 		Vault: &esv1beta1.VaultProvider{},
@@ -116,7 +128,7 @@ type connector struct {
 	newVaultClient func(c *vault.Config) (Client, error)
 }
 
-func (c *connector) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
+func (c *connector) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	storeSpec := store.GetSpec()
 	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Vault == nil {
 		return nil, errors.New(errVaultStore)
@@ -158,6 +170,64 @@ func (c *connector) NewClient(ctx context.Context, store esv1beta1.GenericStore,
 	return vStore, nil
 }
 
+func (c *connector) ValidateStore(store esv1beta1.GenericStore) error {
+	if store == nil {
+		return fmt.Errorf(errInvalidStore)
+	}
+	spc := store.GetSpec()
+	if spc == nil {
+		return fmt.Errorf(errInvalidStoreSpec)
+	}
+	if spc.Provider == nil {
+		return fmt.Errorf(errInvalidStoreProv)
+	}
+	p := spc.Provider.Vault
+	if p == nil {
+		return fmt.Errorf(errInvalidVaultProv)
+	}
+	if p.Auth.AppRole != nil {
+		if err := utils.ValidateSecretSelector(store, p.Auth.AppRole.SecretRef); err != nil {
+			return fmt.Errorf(errInvalidAppRoleSec, err)
+		}
+	}
+	if p.Auth.Cert != nil {
+		if err := utils.ValidateSecretSelector(store, p.Auth.Cert.ClientCert); err != nil {
+			return fmt.Errorf(errInvalidClientCert, err)
+		}
+		if err := utils.ValidateSecretSelector(store, p.Auth.Cert.SecretRef); err != nil {
+			return fmt.Errorf(errInvalidCertSec, err)
+		}
+	}
+	if p.Auth.Jwt != nil {
+		if err := utils.ValidateSecretSelector(store, p.Auth.Jwt.SecretRef); err != nil {
+			return fmt.Errorf(errInvalidJwtSec, err)
+		}
+	}
+	if p.Auth.Kubernetes != nil {
+		if p.Auth.Kubernetes.ServiceAccountRef != nil {
+			if err := utils.ValidateServiceAccountSelector(store, *p.Auth.Kubernetes.ServiceAccountRef); err != nil {
+				return fmt.Errorf(errInvalidKubeSA, err)
+			}
+		}
+		if p.Auth.Kubernetes.SecretRef != nil {
+			if err := utils.ValidateSecretSelector(store, *p.Auth.Kubernetes.SecretRef); err != nil {
+				return fmt.Errorf(errInvalidKubeSec, err)
+			}
+		}
+	}
+	if p.Auth.Ldap != nil {
+		if err := utils.ValidateSecretSelector(store, p.Auth.Ldap.SecretRef); err != nil {
+			return fmt.Errorf(errInvalidLdapSec, err)
+		}
+	}
+	if p.Auth.TokenSecretRef != nil {
+		if err := utils.ValidateSecretSelector(store, *p.Auth.TokenSecretRef); err != nil {
+			return fmt.Errorf(errInvalidTokenRef, err)
+		}
+	}
+	return nil
+}
+
 // Empty GetAllSecrets.
 func (v *client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
 	// TO be implemented

+ 140 - 0
pkg/provider/vault/vault_test.go

@@ -29,6 +29,7 @@ import (
 	vault "github.com/hashicorp/vault/api"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/pointer"
 	kclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
@@ -1070,3 +1071,142 @@ func TestGetSecretPath(t *testing.T) {
 		})
 	}
 }
+
+func TestValidateStore(t *testing.T) {
+	type args struct {
+		auth esv1beta1.VaultAuth
+	}
+
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "empty auth",
+			args: args{},
+		},
+
+		{
+			name: "invalid approle with namespace",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					AppRole: &esv1beta1.VaultAppRole{
+						SecretRef: esmeta.SecretKeySelector{
+							Namespace: pointer.StringPtr("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid clientcert",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					Cert: &esv1beta1.VaultCertAuth{
+						ClientCert: esmeta.SecretKeySelector{
+							Namespace: pointer.StringPtr("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid cert secret",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					Cert: &esv1beta1.VaultCertAuth{
+						SecretRef: esmeta.SecretKeySelector{
+							Namespace: pointer.StringPtr("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid jwt secret",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					Jwt: &esv1beta1.VaultJwtAuth{
+						SecretRef: esmeta.SecretKeySelector{
+							Namespace: pointer.StringPtr("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid kubernetes sa",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					Kubernetes: &esv1beta1.VaultKubernetesAuth{
+						ServiceAccountRef: &esmeta.ServiceAccountSelector{
+							Namespace: pointer.StringPtr("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid kubernetes secret",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					Kubernetes: &esv1beta1.VaultKubernetesAuth{
+						SecretRef: &esmeta.SecretKeySelector{
+							Namespace: pointer.StringPtr("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid ldap secret",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					Ldap: &esv1beta1.VaultLdapAuth{
+						SecretRef: esmeta.SecretKeySelector{
+							Namespace: pointer.StringPtr("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid token secret",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					TokenSecretRef: &esmeta.SecretKeySelector{
+						Namespace: pointer.StringPtr("invalid"),
+					},
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := &connector{
+				newVaultClient: nil,
+			}
+			store := &esv1beta1.SecretStore{
+				Spec: esv1beta1.SecretStoreSpec{
+					Provider: &esv1beta1.SecretStoreProvider{
+						Vault: &esv1beta1.VaultProvider{
+							Auth: tt.args.auth,
+						},
+					},
+				},
+			}
+			if err := c.ValidateStore(store); (err != nil) != tt.wantErr {
+				t.Errorf("connector.ValidateStore() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}

+ 6 - 4
pkg/provider/webhook/webhook.go

@@ -34,8 +34,6 @@ import (
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 	"github.com/external-secrets/external-secrets/pkg/template/v2"
 )
 
@@ -51,12 +49,12 @@ type WebHook struct {
 }
 
 func init() {
-	schema.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+	esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
 		Webhook: &esv1beta1.WebhookProvider{},
 	})
 }
 
-func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
+func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	whClient := &WebHook{
 		kube:      kube,
 		store:     store,
@@ -74,6 +72,10 @@ func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore,
 	return whClient, nil
 }
 
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
+	return nil
+}
+
 func getProvider(store esv1beta1.GenericStore) (*esv1beta1.WebhookProvider, error) {
 	spc := store.GetSpec()
 	if spc == nil || spc.Provider == nil || spc.Provider.Webhook == nil {

+ 2 - 3
pkg/provider/webhook/webhook_test.go

@@ -28,7 +28,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
 )
 
 type testCase struct {
@@ -268,7 +267,7 @@ func runTestCase(tc testCase, t *testing.T) {
 	}
 }
 
-func testGetSecretMap(tc testCase, t *testing.T, client provider.SecretsClient) {
+func testGetSecretMap(tc testCase, t *testing.T, client esv1beta1.SecretsClient) {
 	testRef := esv1beta1.ExternalSecretDataRemoteRef{
 		Key:     tc.Args.Key,
 		Version: tc.Args.Version,
@@ -293,7 +292,7 @@ func testGetSecretMap(tc testCase, t *testing.T, client provider.SecretsClient)
 	}
 }
 
-func testGetSecret(tc testCase, t *testing.T, client provider.SecretsClient) {
+func testGetSecret(tc testCase, t *testing.T, client esv1beta1.SecretsClient) {
 	testRef := esv1beta1.ExternalSecretDataRemoteRef{
 		Key:     tc.Args.Key,
 		Version: tc.Args.Version,

+ 6 - 4
pkg/provider/yandex/lockbox/lockbox.go

@@ -30,8 +30,6 @@ import (
 	kclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-	"github.com/external-secrets/external-secrets/pkg/provider"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
 	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/grpc"
 )
@@ -66,7 +64,7 @@ func newLockboxProvider(yandexCloudCreator client.YandexCloudCreator) *lockboxPr
 }
 
 // NewClient constructs a Yandex Lockbox Provider.
-func (p *lockboxProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
+func (p *lockboxProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	storeSpec := store.GetSpec()
 	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.YandexLockbox == nil {
 		return nil, fmt.Errorf("received invalid Yandex Lockbox SecretStore resource")
@@ -218,6 +216,10 @@ func (p *lockboxProvider) cleanUpIamTokenMap() {
 	}
 }
 
+func (p *lockboxProvider) ValidateStore(store esv1beta1.GenericStore) error {
+	return nil
+}
+
 // lockboxSecretsClient is a secrets client for Yandex Lockbox.
 type lockboxSecretsClient struct {
 	lockboxClient client.LockboxClient
@@ -327,7 +329,7 @@ func init() {
 		}
 	}()
 
-	schema.Register(
+	esv1beta1.Register(
 		lockboxProvider,
 		&esv1beta1.SecretStoreProvider{
 			YandexLockbox: &esv1beta1.YandexLockboxProvider{},

+ 1 - 2
pkg/provider/yandex/lockbox/lockbox_test.go

@@ -34,7 +34,6 @@ import (
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
-	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/fake"
 )
 
@@ -58,7 +57,7 @@ func TestNewClient(t *testing.T) {
 			},
 		},
 	}
-	provider, err := schema.GetProvider(store)
+	provider, err := esv1beta1.GetProvider(store)
 	tassert.Nil(t, err)
 
 	k8sClient := clientfake.NewClientBuilder().Build()

+ 31 - 0
pkg/utils/utils.go

@@ -21,6 +21,9 @@ import (
 	"fmt"
 	"reflect"
 	"strings"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 )
 
 // MergeByteMap merges map of byte slices.
@@ -66,3 +69,31 @@ func ErrorContains(out error, want string) bool {
 	}
 	return strings.Contains(out.Error(), want)
 }
+
+// ValidateSecretSelector just checks if the namespace field is present/absent
+// depending on the secret store type.
+// We MUST NOT check the name or key property here. It MAY be defaulted by the provider.
+func ValidateSecretSelector(store esv1beta1.GenericStore, ref esmeta.SecretKeySelector) error {
+	clusterScope := store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind
+	if clusterScope && ref.Namespace == nil {
+		return fmt.Errorf("cluster scope requires namespace")
+	}
+	if !clusterScope && ref.Namespace != nil {
+		return fmt.Errorf("namespace not allowed with namespaced SecretStore")
+	}
+	return nil
+}
+
+// ValidateServiceAccountSelector just checks if the namespace field is present/absent
+// depending on the secret store type.
+// We MUST NOT check the name or key property here. It MAY be defaulted by the provider.
+func ValidateServiceAccountSelector(store esv1beta1.GenericStore, ref esmeta.ServiceAccountSelector) error {
+	clusterScope := store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind
+	if clusterScope && ref.Namespace == nil {
+		return fmt.Errorf("cluster scope requires namespace")
+	}
+	if !clusterScope && ref.Namespace != nil {
+		return fmt.Errorf("namespace not allowed with namespaced SecretStore")
+	}
+	return nil
+}