Browse Source

Initial draft of reporter (#466)

* Initial draft of reporter

* Test out reporter in AWS provider

* trying out different events approach

* feat: implement store reconciler and events

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

* feat: add validate() method to provider interface

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

* fix: use static requeue interval in store ctrl

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

Co-authored-by: Mircea Cosbuc <mircea.cosbuc@container-solutions.com>
Co-authored-by: Moritz Johner <beller.moritz@googlemail.com>
Lucas Severo Alves 4 years ago
parent
commit
6630ab7494
32 changed files with 732 additions and 53 deletions
  1. 5 0
      apis/externalsecrets/v1alpha1/externalsecret_types.go
  2. 29 0
      apis/externalsecrets/v1alpha1/generic_store.go
  3. 6 0
      apis/externalsecrets/v1alpha1/secretstore_types.go
  4. 6 0
      deploy/charts/external-secrets/templates/rbac.yaml
  5. 3 0
      deploy/crds/external-secrets.io_secretstores.yaml
  6. 18 3
      main.go
  7. 15 14
      pkg/controllers/externalsecret/externalsecret_controller.go
  8. 2 1
      pkg/controllers/externalsecret/suite_test.go
  9. 65 0
      pkg/controllers/secretstore/clustersecretstore_controller.go
  10. 114 0
      pkg/controllers/secretstore/common.go
  11. 213 0
      pkg/controllers/secretstore/common_test.go
  12. 26 11
      pkg/controllers/secretstore/secretstore_controller.go
  13. 35 7
      pkg/controllers/secretstore/suite_test.go
  14. 75 0
      pkg/controllers/secretstore/util.go
  15. 4 0
      pkg/provider/akeyless/akeyless.go
  16. 4 0
      pkg/provider/alibaba/kms.go
  17. 9 2
      pkg/provider/aws/parameterstore/parameterstore.go
  18. 2 0
      pkg/provider/aws/provider.go
  19. 9 0
      pkg/provider/aws/provider_test.go
  20. 9 2
      pkg/provider/aws/secretsmanager/secretsmanager.go
  21. 4 0
      pkg/provider/azure/keyvault/keyvault.go
  22. 4 0
      pkg/provider/fake/fake.go
  23. 11 0
      pkg/provider/gcp/secretmanager/secretsmanager.go
  24. 4 0
      pkg/provider/gitlab/gitlab.go
  25. 4 0
      pkg/provider/ibm/provider.go
  26. 4 0
      pkg/provider/oracle/oracle.go
  27. 4 0
      pkg/provider/provider.go
  28. 4 0
      pkg/provider/schema/schema_test.go
  29. 5 0
      pkg/provider/testing/fake/fake.go
  30. 31 13
      pkg/provider/vault/vault.go
  31. 4 0
      pkg/provider/webhook/webhook.go
  32. 4 0
      pkg/provider/yandex/lockbox/lockbox.go

+ 5 - 0
apis/externalsecrets/v1alpha1/externalsecret_types.go

@@ -180,6 +180,11 @@ const (
 	ConditionReasonSecretSyncedError = "SecretSyncedError"
 	ConditionReasonSecretSyncedError = "SecretSyncedError"
 	// ConditionReasonSecretDeleted indicates that the secret has been deleted.
 	// ConditionReasonSecretDeleted indicates that the secret has been deleted.
 	ConditionReasonSecretDeleted = "SecretDeleted"
 	ConditionReasonSecretDeleted = "SecretDeleted"
+
+	ReasonInvalidStoreRef      = "InvalidStoreRef"
+	ReasonProviderClientConfig = "InvalidProviderClientConfig"
+	ReasonUpdateFailed         = "UpdateFailed"
+	ReasonUpdated              = "Updated"
 )
 )
 
 
 type ExternalSecretStatus struct {
 type ExternalSecretStatus struct {

+ 29 - 0
apis/externalsecrets/v1alpha1/generic_store.go

@@ -33,8 +33,13 @@ type GenericStore interface {
 	metav1.Object
 	metav1.Object
 
 
 	GetObjectMeta() *metav1.ObjectMeta
 	GetObjectMeta() *metav1.ObjectMeta
+	GetTypeMeta() *metav1.TypeMeta
+
 	GetSpec() *SecretStoreSpec
 	GetSpec() *SecretStoreSpec
 	GetNamespacedName() string
 	GetNamespacedName() string
+	GetStatus() SecretStoreStatus
+	SetStatus(status SecretStoreStatus)
+	Copy() GenericStore
 }
 }
 
 
 // +kubebuilder:object:root:false
 // +kubebuilder:object:root:false
@@ -45,10 +50,22 @@ func (c *SecretStore) GetObjectMeta() *metav1.ObjectMeta {
 	return &c.ObjectMeta
 	return &c.ObjectMeta
 }
 }
 
 
+func (c *SecretStore) GetTypeMeta() *metav1.TypeMeta {
+	return &c.TypeMeta
+}
+
 func (c *SecretStore) GetSpec() *SecretStoreSpec {
 func (c *SecretStore) GetSpec() *SecretStoreSpec {
 	return &c.Spec
 	return &c.Spec
 }
 }
 
 
+func (c *SecretStore) GetStatus() SecretStoreStatus {
+	return c.Status
+}
+
+func (c *SecretStore) SetStatus(status SecretStoreStatus) {
+	c.Status = status
+}
+
 func (c *SecretStore) GetNamespacedName() string {
 func (c *SecretStore) GetNamespacedName() string {
 	return fmt.Sprintf("%s/%s", c.Namespace, c.Name)
 	return fmt.Sprintf("%s/%s", c.Namespace, c.Name)
 }
 }
@@ -65,6 +82,10 @@ func (c *ClusterSecretStore) GetObjectMeta() *metav1.ObjectMeta {
 	return &c.ObjectMeta
 	return &c.ObjectMeta
 }
 }
 
 
+func (c *ClusterSecretStore) GetTypeMeta() *metav1.TypeMeta {
+	return &c.TypeMeta
+}
+
 func (c *ClusterSecretStore) GetSpec() *SecretStoreSpec {
 func (c *ClusterSecretStore) GetSpec() *SecretStoreSpec {
 	return &c.Spec
 	return &c.Spec
 }
 }
@@ -73,6 +94,14 @@ func (c *ClusterSecretStore) Copy() GenericStore {
 	return c.DeepCopy()
 	return c.DeepCopy()
 }
 }
 
 
+func (c *ClusterSecretStore) GetStatus() SecretStoreStatus {
+	return c.Status
+}
+
+func (c *ClusterSecretStore) SetStatus(status SecretStoreStatus) {
+	c.Status = status
+}
+
 func (c *ClusterSecretStore) GetNamespacedName() string {
 func (c *ClusterSecretStore) GetNamespacedName() string {
 	return fmt.Sprintf("%s/%s", c.Namespace, c.Name)
 	return fmt.Sprintf("%s/%s", c.Namespace, c.Name)
 }
 }

+ 6 - 0
apis/externalsecrets/v1alpha1/secretstore_types.go

@@ -96,6 +96,11 @@ type SecretStoreConditionType string
 
 
 const (
 const (
 	SecretStoreReady SecretStoreConditionType = "Ready"
 	SecretStoreReady SecretStoreConditionType = "Ready"
+
+	ReasonInvalidStore          = "InvalidStoreConfiguration"
+	ReasonInvalidProviderConfig = "InvalidProviderConfig"
+	ReasonValidationFailed      = "ValidationFailed"
+	ReasonStoreValid            = "Valid"
 )
 )
 
 
 type SecretStoreStatusCondition struct {
 type SecretStoreStatusCondition struct {
@@ -122,6 +127,7 @@ type SecretStoreStatus struct {
 
 
 // SecretStore represents a secure external location for storing secrets, which can be referenced as part of `storeRef` fields.
 // SecretStore represents a secure external location for storing secrets, which can be referenced as part of `storeRef` fields.
 // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
 // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
+// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`
 // +kubebuilder:subresource:status
 // +kubebuilder:subresource:status
 // +kubebuilder:resource:scope=Namespaced,categories={externalsecrets},shortName=ss
 // +kubebuilder:resource:scope=Namespaced,categories={externalsecrets},shortName=ss
 type SecretStore struct {
 type SecretStore struct {

+ 6 - 0
deploy/charts/external-secrets/templates/rbac.yaml

@@ -22,6 +22,12 @@ rules:
     - "externalsecrets"
     - "externalsecrets"
     - "externalsecrets/status"
     - "externalsecrets/status"
     - "externalsecrets/finalizers"
     - "externalsecrets/finalizers"
+    - "secretstores"
+    - "secretstores/status"
+    - "secretstores/finalizers"
+    - "clustersecretstores"
+    - "clustersecretstores/status"
+    - "clustersecretstores/finalizers"
     verbs:
     verbs:
     - "update"
     - "update"
     - "patch"
     - "patch"

+ 3 - 0
deploy/crds/external-secrets.io_secretstores.yaml

@@ -22,6 +22,9 @@ spec:
     - jsonPath: .metadata.creationTimestamp
     - jsonPath: .metadata.creationTimestamp
       name: AGE
       name: AGE
       type: date
       type: date
+    - jsonPath: .status.conditions[?(@.type=="Ready")].reason
+      name: Status
+      type: string
     name: v1alpha1
     name: v1alpha1
     schema:
     schema:
       openAPIV3Schema:
       openAPIV3Schema:

+ 18 - 3
main.go

@@ -37,6 +37,8 @@ var (
 	setupLog = ctrl.Log.WithName("setup")
 	setupLog = ctrl.Log.WithName("setup")
 )
 )
 
 
+const errCreateController = "unable to create controller"
+
 func init() {
 func init() {
 	_ = clientgoscheme.AddToScheme(scheme)
 	_ = clientgoscheme.AddToScheme(scheme)
 	_ = esv1alpha1.AddToScheme(scheme)
 	_ = esv1alpha1.AddToScheme(scheme)
@@ -49,6 +51,7 @@ func main() {
 	var concurrent int
 	var concurrent int
 	var loglevel string
 	var loglevel string
 	var namespace string
 	var namespace string
+	var storeRequeueInterval time.Duration
 	flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
 	flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
 	flag.StringVar(&controllerClass, "controller-class", "default", "the controller is instantiated with a specific controller name and filters ES based on this property")
 	flag.StringVar(&controllerClass, "controller-class", "default", "the controller is instantiated with a specific controller name and filters ES based on this property")
 	flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
 	flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
@@ -57,6 +60,7 @@ func main() {
 	flag.IntVar(&concurrent, "concurrent", 1, "The number of concurrent ExternalSecret reconciles.")
 	flag.IntVar(&concurrent, "concurrent", 1, "The number of concurrent ExternalSecret reconciles.")
 	flag.StringVar(&loglevel, "loglevel", "info", "loglevel to use, one of: debug, info, warn, error, dpanic, panic, fatal")
 	flag.StringVar(&loglevel, "loglevel", "info", "loglevel to use, one of: debug, info, warn, error, dpanic, panic, fatal")
 	flag.StringVar(&namespace, "namespace", "", "watch external secrets scoped in the provided namespace only. ClusterSecretStore can be used but only work if it doesn't reference resources from other namespaces")
 	flag.StringVar(&namespace, "namespace", "", "watch external secrets scoped in the provided namespace only. ClusterSecretStore can be used but only work if it doesn't reference resources from other namespaces")
+	flag.DurationVar(&storeRequeueInterval, "store-requeue-interval", time.Minute*5, "Time duration between reconciling (Cluster)SecretStores")
 	flag.Parse()
 	flag.Parse()
 
 
 	var lvl zapcore.Level
 	var lvl zapcore.Level
@@ -81,13 +85,24 @@ func main() {
 		os.Exit(1)
 		os.Exit(1)
 	}
 	}
 
 
-	if err = (&secretstore.Reconciler{
+	if err = (&secretstore.StoreReconciler{
 		Client:          mgr.GetClient(),
 		Client:          mgr.GetClient(),
 		Log:             ctrl.Log.WithName("controllers").WithName("SecretStore"),
 		Log:             ctrl.Log.WithName("controllers").WithName("SecretStore"),
 		Scheme:          mgr.GetScheme(),
 		Scheme:          mgr.GetScheme(),
 		ControllerClass: controllerClass,
 		ControllerClass: controllerClass,
+		RequeueInterval: storeRequeueInterval,
+	}).SetupWithManager(mgr); err != nil {
+		setupLog.Error(err, errCreateController, "controller", "SecretStore")
+		os.Exit(1)
+	}
+	if err = (&secretstore.ClusterStoreReconciler{
+		Client:          mgr.GetClient(),
+		Log:             ctrl.Log.WithName("controllers").WithName("ClusterSecretStore"),
+		Scheme:          mgr.GetScheme(),
+		ControllerClass: controllerClass,
+		RequeueInterval: storeRequeueInterval,
 	}).SetupWithManager(mgr); err != nil {
 	}).SetupWithManager(mgr); err != nil {
-		setupLog.Error(err, "unable to create controller", "controller", "SecretStore")
+		setupLog.Error(err, errCreateController, "controller", "ClusterSecretStore")
 		os.Exit(1)
 		os.Exit(1)
 	}
 	}
 	if err = (&externalsecret.Reconciler{
 	if err = (&externalsecret.Reconciler{
@@ -99,7 +114,7 @@ func main() {
 	}).SetupWithManager(mgr, controller.Options{
 	}).SetupWithManager(mgr, controller.Options{
 		MaxConcurrentReconciles: concurrent,
 		MaxConcurrentReconciles: concurrent,
 	}); err != nil {
 	}); err != nil {
-		setupLog.Error(err, "unable to create controller", "controller", "ExternalSecret")
+		setupLog.Error(err, errCreateController, "controller", "ExternalSecret")
 		os.Exit(1)
 		os.Exit(1)
 	}
 	}
 
 

+ 15 - 14
pkg/controllers/externalsecret/externalsecret_controller.go

@@ -27,12 +27,14 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/tools/record"
 	ctrl "sigs.k8s.io/controller-runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
 
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
 	"github.com/external-secrets/external-secrets/pkg/provider"
 	"github.com/external-secrets/external-secrets/pkg/provider"
 
 
 	// Loading registered providers.
 	// Loading registered providers.
@@ -45,7 +47,7 @@ const (
 	requeueAfter = time.Second * 30
 	requeueAfter = time.Second * 30
 
 
 	errGetES                 = "could not get ExternalSecret"
 	errGetES                 = "could not get ExternalSecret"
-	errReconcileES           = "could not reconcile ExternalSecret"
+	errUpdateSecret          = "could not update Secret"
 	errPatchStatus           = "unable to patch status"
 	errPatchStatus           = "unable to patch status"
 	errGetSecretStore        = "could not get SecretStore %q, %w"
 	errGetSecretStore        = "could not get SecretStore %q, %w"
 	errGetClusterSecretStore = "could not get ClusterSecretStore %q, %w"
 	errGetClusterSecretStore = "could not get ClusterSecretStore %q, %w"
@@ -75,6 +77,7 @@ type Reconciler struct {
 	Scheme          *runtime.Scheme
 	Scheme          *runtime.Scheme
 	ControllerClass string
 	ControllerClass string
 	RequeueInterval time.Duration
 	RequeueInterval time.Duration
+	recorder        record.EventRecorder
 }
 }
 
 
 // Reconcile implements the main reconciliation loop
 // Reconcile implements the main reconciliation loop
@@ -116,7 +119,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 	store, err := r.getStore(ctx, &externalSecret)
 	store, err := r.getStore(ctx, &externalSecret)
 	if err != nil {
 	if err != nil {
 		log.Error(err, errStoreRef)
 		log.Error(err, errStoreRef)
-		conditionSynced := NewExternalSecretCondition(esv1alpha1.ExternalSecretReady, v1.ConditionFalse, esv1alpha1.ConditionReasonSecretSyncedError, err.Error())
+		r.recorder.Event(&externalSecret, v1.EventTypeWarning, esv1alpha1.ReasonInvalidStoreRef, err.Error())
+		conditionSynced := NewExternalSecretCondition(esv1alpha1.ExternalSecretReady, v1.ConditionFalse, esv1alpha1.ConditionReasonSecretSyncedError, errStoreRef)
 		SetExternalSecretCondition(&externalSecret, *conditionSynced)
 		SetExternalSecretCondition(&externalSecret, *conditionSynced)
 		syncCallsError.With(syncCallsMetricLabels).Inc()
 		syncCallsError.With(syncCallsMetricLabels).Inc()
 		return ctrl.Result{RequeueAfter: requeueAfter}, nil
 		return ctrl.Result{RequeueAfter: requeueAfter}, nil
@@ -125,7 +129,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 	log = log.WithValues("SecretStore", store.GetNamespacedName())
 	log = log.WithValues("SecretStore", store.GetNamespacedName())
 
 
 	// check if store should be handled by this controller instance
 	// check if store should be handled by this controller instance
-	if !shouldProcessStore(store, r.ControllerClass) {
+	if !secretstore.ShouldProcessStore(store, r.ControllerClass) {
 		log.Info("skipping unmanaged store")
 		log.Info("skipping unmanaged store")
 		return ctrl.Result{}, nil
 		return ctrl.Result{}, nil
 	}
 	}
@@ -140,8 +144,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 	secretClient, err := storeProvider.NewClient(ctx, store, r.Client, req.Namespace)
 	secretClient, err := storeProvider.NewClient(ctx, store, r.Client, req.Namespace)
 	if err != nil {
 	if err != nil {
 		log.Error(err, errStoreClient)
 		log.Error(err, errStoreClient)
-		conditionSynced := NewExternalSecretCondition(esv1alpha1.ExternalSecretReady, v1.ConditionFalse, esv1alpha1.ConditionReasonSecretSyncedError, err.Error())
+		conditionSynced := NewExternalSecretCondition(esv1alpha1.ExternalSecretReady, v1.ConditionFalse, esv1alpha1.ConditionReasonSecretSyncedError, errStoreClient)
 		SetExternalSecretCondition(&externalSecret, *conditionSynced)
 		SetExternalSecretCondition(&externalSecret, *conditionSynced)
+		r.recorder.Event(&externalSecret, v1.EventTypeWarning, esv1alpha1.ReasonProviderClientConfig, err.Error())
 		syncCallsError.With(syncCallsMetricLabels).Inc()
 		syncCallsError.With(syncCallsMetricLabels).Inc()
 		return ctrl.Result{RequeueAfter: requeueAfter}, nil
 		return ctrl.Result{RequeueAfter: requeueAfter}, nil
 	}
 	}
@@ -232,13 +237,15 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 	}
 	}
 
 
 	if err != nil {
 	if err != nil {
-		log.Error(err, errReconcileES)
-		conditionSynced := NewExternalSecretCondition(esv1alpha1.ExternalSecretReady, v1.ConditionFalse, esv1alpha1.ConditionReasonSecretSyncedError, err.Error())
+		log.Error(err, errUpdateSecret)
+		r.recorder.Event(&externalSecret, v1.EventTypeWarning, esv1alpha1.ReasonUpdateFailed, err.Error())
+		conditionSynced := NewExternalSecretCondition(esv1alpha1.ExternalSecretReady, v1.ConditionFalse, esv1alpha1.ConditionReasonSecretSyncedError, errUpdateSecret)
 		SetExternalSecretCondition(&externalSecret, *conditionSynced)
 		SetExternalSecretCondition(&externalSecret, *conditionSynced)
 		syncCallsError.With(syncCallsMetricLabels).Inc()
 		syncCallsError.With(syncCallsMetricLabels).Inc()
 		return ctrl.Result{RequeueAfter: requeueAfter}, nil
 		return ctrl.Result{RequeueAfter: requeueAfter}, nil
 	}
 	}
 
 
+	r.recorder.Event(&externalSecret, v1.EventTypeNormal, esv1alpha1.ReasonUpdated, "Updated Secret")
 	conditionSynced := NewExternalSecretCondition(esv1alpha1.ExternalSecretReady, v1.ConditionTrue, esv1alpha1.ConditionReasonSecretSynced, "Secret was synced")
 	conditionSynced := NewExternalSecretCondition(esv1alpha1.ExternalSecretReady, v1.ConditionTrue, esv1alpha1.ConditionReasonSecretSynced, "Secret was synced")
 	currCond := GetExternalSecretCondition(externalSecret.Status, esv1alpha1.ExternalSecretReady)
 	currCond := GetExternalSecretCondition(externalSecret.Status, esv1alpha1.ExternalSecretReady)
 	SetExternalSecretCondition(&externalSecret, *conditionSynced)
 	SetExternalSecretCondition(&externalSecret, *conditionSynced)
@@ -297,14 +304,6 @@ func patchSecret(ctx context.Context, c client.Client, scheme *runtime.Scheme, s
 	return nil
 	return nil
 }
 }
 
 
-// shouldProcessStore returns true if the store should be processed.
-func shouldProcessStore(store esv1alpha1.GenericStore, class string) bool {
-	if store.GetSpec().Controller == "" || store.GetSpec().Controller == class {
-		return true
-	}
-	return false
-}
-
 func getResourceVersion(es esv1alpha1.ExternalSecret) string {
 func getResourceVersion(es esv1alpha1.ExternalSecret) string {
 	return fmt.Sprintf("%d-%s", es.ObjectMeta.GetGeneration(), hashMeta(es.ObjectMeta))
 	return fmt.Sprintf("%d-%s", es.ObjectMeta.GetGeneration(), hashMeta(es.ObjectMeta))
 }
 }
@@ -419,6 +418,8 @@ func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient p
 
 
 // SetupWithManager returns a new controller builder that will be started by the provided Manager.
 // SetupWithManager returns a new controller builder that will be started by the provided Manager.
 func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
 func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
+	r.recorder = mgr.GetEventRecorderFor("external-secrets")
+
 	return ctrl.NewControllerManagedBy(mgr).
 	return ctrl.NewControllerManagedBy(mgr).
 		WithOptions(opts).
 		WithOptions(opts).
 		For(&esv1alpha1.ExternalSecret{}).
 		For(&esv1alpha1.ExternalSecret{}).

+ 2 - 1
pkg/controllers/externalsecret/suite_test.go

@@ -70,7 +70,8 @@ var _ = BeforeSuite(func() {
 	Expect(err).NotTo(HaveOccurred())
 	Expect(err).NotTo(HaveOccurred())
 
 
 	k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
 	k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
-		Scheme: scheme.Scheme,
+		Scheme:             scheme.Scheme,
+		MetricsBindAddress: "0", // avoid port collision when testing
 	})
 	})
 	Expect(err).ToNot(HaveOccurred())
 	Expect(err).ToNot(HaveOccurred())
 
 

+ 65 - 0
pkg/controllers/secretstore/clustersecretstore_controller.go

@@ -0,0 +1,65 @@
+/*
+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 secretstore
+
+import (
+	"context"
+	"time"
+
+	"github.com/go-logr/logr"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/tools/record"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+
+	// Loading registered providers.
+	_ "github.com/external-secrets/external-secrets/pkg/provider/register"
+)
+
+// ClusterStoreReconciler reconciles a SecretStore object.
+type ClusterStoreReconciler struct {
+	client.Client
+	Log             logr.Logger
+	Scheme          *runtime.Scheme
+	ControllerClass string
+	RequeueInterval time.Duration
+	recorder        record.EventRecorder
+}
+
+func (r *ClusterStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	log := r.Log.WithValues("clustersecretstore", req.NamespacedName)
+	var css esapi.ClusterSecretStore
+	err := r.Get(ctx, req.NamespacedName, &css)
+	if apierrors.IsNotFound(err) {
+		return ctrl.Result{}, nil
+	} else if err != nil {
+		log.Error(err, "unable to get ClusterSecretStore")
+		return ctrl.Result{}, err
+	}
+
+	return reconcile(ctx, req, &css, r.Client, log, r.ControllerClass, r.recorder, r.RequeueInterval)
+}
+
+// SetupWithManager returns a new controller builder that will be started by the provided Manager.
+func (r *ClusterStoreReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	r.recorder = mgr.GetEventRecorderFor("cluster-secret-store")
+
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&esapi.ClusterSecretStore{}).
+		Complete(r)
+}

+ 114 - 0
pkg/controllers/secretstore/common.go

@@ -0,0 +1,114 @@
+/*
+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 secretstore
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/go-logr/logr"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/client-go/tools/record"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"github.com/external-secrets/external-secrets/pkg/provider/schema"
+)
+
+const (
+	errStoreProvider       = "could not get store provider: %w"
+	errStoreClient         = "could not get provider client: %w"
+	errValidationFailed    = "could not validate provider: %w"
+	errPatchStatus         = "unable to patch status: %w"
+	errUnableCreateClient  = "unable to create client"
+	errUnableValidateStore = "unable to validate store"
+	errUnableGetProvider   = "unable to get store provider"
+
+	msgStoreValidated = "store validated"
+)
+
+func reconcile(ctx context.Context, req ctrl.Request, ss esapi.GenericStore, cl client.Client,
+	log logr.Logger, controllerClass string, recorder record.EventRecorder, requeueInterval time.Duration) (ctrl.Result, error) {
+	if !ShouldProcessStore(ss, controllerClass) {
+		log.V(1).Info("skip store")
+		return ctrl.Result{}, nil
+	}
+
+	// patch status when done processing
+	p := client.MergeFrom(ss.Copy())
+	defer func() {
+		err := cl.Status().Patch(ctx, ss, p)
+		if err != nil {
+			log.Error(err, errPatchStatus)
+		}
+	}()
+
+	// validateStore modifies the store conditions
+	// we have to patch the status
+	log.V(1).Info("validating")
+	err := validateStore(ctx, req.Namespace, ss, cl, recorder)
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+
+	recorder.Event(ss, v1.EventTypeNormal, esapi.ReasonStoreValid, msgStoreValidated)
+	cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionTrue, esapi.ReasonStoreValid, msgStoreValidated)
+	SetExternalSecretCondition(ss, *cond)
+
+	return ctrl.Result{
+		RequeueAfter: requeueInterval,
+	}, err
+}
+
+// validateStore tries to construct a new client
+// 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)
+	if err != nil {
+		cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionFalse, esapi.ReasonInvalidStore, errUnableGetProvider)
+		SetExternalSecretCondition(store, *cond)
+		recorder.Event(store, v1.EventTypeWarning, esapi.ReasonInvalidStore, err.Error())
+		return fmt.Errorf(errStoreProvider, err)
+	}
+
+	cl, err := storeProvider.NewClient(ctx, store, client, namespace)
+	if err != nil {
+		cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionFalse, esapi.ReasonInvalidProviderConfig, errUnableCreateClient)
+		SetExternalSecretCondition(store, *cond)
+		recorder.Event(store, v1.EventTypeWarning, esapi.ReasonInvalidProviderConfig, err.Error())
+		return fmt.Errorf(errStoreClient, err)
+	}
+
+	err = cl.Validate()
+	if err != nil {
+		cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionFalse, esapi.ReasonValidationFailed, errUnableValidateStore)
+		SetExternalSecretCondition(store, *cond)
+		recorder.Event(store, v1.EventTypeWarning, esapi.ReasonValidationFailed, err.Error())
+		return fmt.Errorf(errValidationFailed, err)
+	}
+
+	return nil
+}
+
+// ShouldProcessStore returns true if the store should be processed.
+func ShouldProcessStore(store esapi.GenericStore, class string) bool {
+	if store.GetSpec().Controller == "" || store.GetSpec().Controller == class {
+		return true
+	}
+
+	return false
+}

+ 213 - 0
pkg/controllers/secretstore/common_test.go

@@ -0,0 +1,213 @@
+/*
+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 secretstore
+
+import (
+	"context"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+)
+
+type testCase struct {
+	store  esapi.GenericStore
+	assert func()
+}
+
+var _ = Describe("SecretStore reconcile", func() {
+	var test *testCase
+
+	BeforeEach(func() {
+		test = makeDefaultTestcase()
+	})
+
+	AfterEach(func() {
+		Expect(k8sClient.Delete(context.Background(), test.store)).ToNot(HaveOccurred())
+	})
+
+	// a invalid provider config should be reflected
+	// in the store status condition
+	invalidProvider := func(tc *testCase) {
+		tc.assert = func() {
+			Eventually(func() bool {
+				ss := tc.store.Copy()
+				err := k8sClient.Get(context.Background(), types.NamespacedName{
+					Name:      defaultStoreName,
+					Namespace: ss.GetObjectMeta().Namespace,
+				}, ss)
+				if err != nil {
+					return false
+				}
+				status := ss.GetStatus()
+				if len(status.Conditions) != 1 {
+					return false
+				}
+				return status.Conditions[0].Reason == esapi.ReasonInvalidProviderConfig &&
+					hasEvent(tc.store.GetTypeMeta().Kind, ss.GetName(), esapi.ReasonInvalidProviderConfig)
+			}).
+				WithTimeout(time.Second * 10).
+				WithPolling(time.Second).
+				Should(BeTrue())
+		}
+	}
+
+	// if controllerClass does not match the controller
+	// should not touch this store
+	ignoreControllerClass := func(tc *testCase) {
+		spc := tc.store.GetSpec()
+		spc.Controller = "something-else"
+		tc.assert = func() {
+			Consistently(func() bool {
+				ss := tc.store.Copy()
+				err := k8sClient.Get(context.Background(), types.NamespacedName{
+					Name:      defaultStoreName,
+					Namespace: ss.GetObjectMeta().Namespace,
+				}, ss)
+				if err != nil {
+					return true
+				}
+				return len(ss.GetStatus().Conditions) == 0
+			}).
+				WithTimeout(time.Second * 3).
+				WithPolling(time.Millisecond * 500).
+				Should(BeTrue())
+		}
+	}
+
+	validProvider := func(tc *testCase) {
+		spc := tc.store.GetSpec()
+		spc.Provider.Vault = nil
+		spc.Provider.Fake = &esapi.FakeProvider{
+			Data: []esapi.FakeProviderData{},
+		}
+
+		tc.assert = func() {
+			Eventually(func() bool {
+				ss := tc.store.Copy()
+				err := k8sClient.Get(context.Background(), types.NamespacedName{
+					Name:      defaultStoreName,
+					Namespace: ss.GetNamespace(),
+				}, ss)
+				if err != nil {
+					return false
+				}
+
+				if len(ss.GetStatus().Conditions) != 1 {
+					return false
+				}
+
+				return ss.GetStatus().Conditions[0].Reason == esapi.ReasonStoreValid &&
+					ss.GetStatus().Conditions[0].Type == esapi.SecretStoreReady &&
+					ss.GetStatus().Conditions[0].Status == corev1.ConditionTrue &&
+					hasEvent(tc.store.GetTypeMeta().Kind, ss.GetName(), esapi.ReasonStoreValid)
+			}).
+				WithTimeout(time.Second * 10).
+				WithPolling(time.Second).
+				Should(BeTrue())
+		}
+
+	}
+
+	DescribeTable("Controller Reconcile logic", func(muts ...func(tc *testCase)) {
+		for _, mut := range muts {
+			mut(test)
+		}
+		err := k8sClient.Create(context.Background(), test.store.Copy())
+		Expect(err).ToNot(HaveOccurred())
+		test.assert()
+	},
+		// namespaced store
+		Entry("[namespace] invalid provider with secretStore should set InvalidStore condition", invalidProvider),
+		Entry("[namespace] ignore stores with non-matching class", ignoreControllerClass),
+		Entry("[namespace] valid provider has status=ready", validProvider),
+
+		// cluster store
+		Entry("[cluster] invalid provider with secretStore should set InvalidStore condition", invalidProvider, useClusterStore),
+		Entry("[cluster] ignore stores with non-matching class", ignoreControllerClass, useClusterStore),
+		Entry("[cluster] valid provider has status=ready", validProvider, useClusterStore),
+	)
+
+})
+
+const (
+	defaultStoreName       = "default-store"
+	defaultControllerClass = "test-ctrl"
+)
+
+func makeDefaultTestcase() *testCase {
+	return &testCase{
+		assert: func() {
+			// this is a noop by default
+		},
+		store: &esapi.SecretStore{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       esapi.SecretStoreKind,
+				APIVersion: esapi.SecretStoreKindAPIVersion,
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      defaultStoreName,
+				Namespace: "default",
+			},
+			Spec: esapi.SecretStoreSpec{
+				Controller: defaultControllerClass,
+				// empty provider
+				// a testCase mutator must fill in the concrete provider
+				Provider: &esapi.SecretStoreProvider{
+					Vault: &esapi.VaultProvider{
+						Version: esapi.VaultKVStoreV1,
+					},
+				},
+			},
+		},
+	}
+}
+
+func useClusterStore(tc *testCase) {
+	spc := tc.store.GetSpec()
+	meta := tc.store.GetObjectMeta()
+
+	tc.store = &esapi.ClusterSecretStore{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       esapi.ClusterSecretStoreKind,
+			APIVersion: esapi.ClusterSecretStoreKindAPIVersion,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name: meta.Name,
+		},
+		Spec: *spc,
+	}
+}
+
+func hasEvent(involvedKind, name, reason string) bool {
+	el := &corev1.EventList{}
+	err := k8sClient.List(context.Background(), el)
+	if err != nil {
+		return false
+	}
+	for i := range el.Items {
+		ev := el.Items[i]
+		if ev.InvolvedObject.Kind == involvedKind && ev.InvolvedObject.Name == name {
+			if ev.Reason == reason {
+				return true
+			}
+		}
+	}
+	return false
+}

+ 26 - 11
pkg/controllers/secretstore/secretstore_controller.go

@@ -16,35 +16,50 @@ package secretstore
 
 
 import (
 import (
 	"context"
 	"context"
+	"time"
 
 
 	"github.com/go-logr/logr"
 	"github.com/go-logr/logr"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/tools/record"
 	ctrl "sigs.k8s.io/controller-runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 
-	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+
+	// Loading registered providers.
+	_ "github.com/external-secrets/external-secrets/pkg/provider/register"
 )
 )
 
 
-// Reconciler reconciles a SecretStore object.
-type Reconciler struct {
+// StoreReconciler reconciles a SecretStore object.
+type StoreReconciler struct {
 	client.Client
 	client.Client
 	Log             logr.Logger
 	Log             logr.Logger
 	Scheme          *runtime.Scheme
 	Scheme          *runtime.Scheme
+	recorder        record.EventRecorder
+	RequeueInterval time.Duration
 	ControllerClass string
 	ControllerClass string
 }
 }
 
 
-func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	_ = context.Background()
-	_ = r.Log.WithValues("secretstore", req.NamespacedName)
-
-	// your logic here
+func (r *StoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	log := r.Log.WithValues("secretstore", req.NamespacedName)
+	var ss esapi.SecretStore
+	err := r.Get(ctx, req.NamespacedName, &ss)
+	if apierrors.IsNotFound(err) {
+		return ctrl.Result{}, nil
+	} else if err != nil {
+		log.Error(err, "unable to get SecretStore")
+		return ctrl.Result{}, err
+	}
 
 
-	return ctrl.Result{}, nil
+	return reconcile(ctx, req, &ss, r.Client, log, r.ControllerClass, r.recorder, r.RequeueInterval)
 }
 }
 
 
 // SetupWithManager returns a new controller builder that will be started by the provided Manager.
 // SetupWithManager returns a new controller builder that will be started by the provided Manager.
-func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
+func (r *StoreReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	r.recorder = mgr.GetEventRecorderFor("secret-store")
+
 	return ctrl.NewControllerManagedBy(mgr).
 	return ctrl.NewControllerManagedBy(mgr).
-		For(&esv1alpha1.SecretStore{}).
+		For(&esapi.SecretStore{}).
 		Complete(r)
 		Complete(r)
 }
 }

+ 35 - 7
pkg/controllers/secretstore/suite_test.go

@@ -15,6 +15,7 @@ limitations under the License.
 package secretstore
 package secretstore
 
 
 import (
 import (
+	"context"
 	"path/filepath"
 	"path/filepath"
 	"testing"
 	"testing"
 
 
@@ -22,20 +23,19 @@ import (
 	. "github.com/onsi/gomega"
 	. "github.com/onsi/gomega"
 	"k8s.io/client-go/kubernetes/scheme"
 	"k8s.io/client-go/kubernetes/scheme"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/rest"
+	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/envtest"
 	"sigs.k8s.io/controller-runtime/pkg/envtest"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 
 
-	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 )
 )
 
 
-// These tests use Ginkgo (BDD-style Go testing framework). Refer to
-// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
-
 var cfg *rest.Config
 var cfg *rest.Config
 var k8sClient client.Client
 var k8sClient client.Client
 var testEnv *envtest.Environment
 var testEnv *envtest.Environment
+var cancel context.CancelFunc
 
 
 func TestAPIs(t *testing.T) {
 func TestAPIs(t *testing.T) {
 	RegisterFailHandler(Fail)
 	RegisterFailHandler(Fail)
@@ -51,24 +51,52 @@ var _ = BeforeSuite(func() {
 		CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "deploy", "crds")},
 		CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "deploy", "crds")},
 	}
 	}
 
 
+	var ctx context.Context
+	ctx, cancel = context.WithCancel(context.Background())
+
 	var err error
 	var err error
 	cfg, err = testEnv.Start()
 	cfg, err = testEnv.Start()
 	Expect(err).ToNot(HaveOccurred())
 	Expect(err).ToNot(HaveOccurred())
 	Expect(cfg).ToNot(BeNil())
 	Expect(cfg).ToNot(BeNil())
 
 
-	err = esv1alpha1.AddToScheme(scheme.Scheme)
+	err = esapi.AddToScheme(scheme.Scheme)
 	Expect(err).NotTo(HaveOccurred())
 	Expect(err).NotTo(HaveOccurred())
 
 
-	err = esv1alpha1.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})
 	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
 	Expect(err).ToNot(HaveOccurred())
 	Expect(err).ToNot(HaveOccurred())
 	Expect(k8sClient).ToNot(BeNil())
 	Expect(k8sClient).ToNot(BeNil())
+
+	err = (&StoreReconciler{
+		Client:          k8sClient,
+		Scheme:          k8sManager.GetScheme(),
+		Log:             ctrl.Log.WithName("controllers").WithName("SecretStore"),
+		ControllerClass: defaultControllerClass,
+	}).SetupWithManager(k8sManager)
+	Expect(err).ToNot(HaveOccurred())
+
+	err = (&ClusterStoreReconciler{
+		Client:          k8sClient,
+		Scheme:          k8sManager.GetScheme(),
+		ControllerClass: defaultControllerClass,
+		Log:             ctrl.Log.WithName("controllers").WithName("ClusterSecretStore"),
+	}).SetupWithManager(k8sManager)
+	Expect(err).ToNot(HaveOccurred())
+
+	go func() {
+		defer GinkgoRecover()
+		Expect(k8sManager.Start(ctx)).ToNot(HaveOccurred())
+	}()
 })
 })
 
 
 var _ = AfterSuite(func() {
 var _ = AfterSuite(func() {
 	By("tearing down the test environment")
 	By("tearing down the test environment")
+	cancel() // stop manager
 	err := testEnv.Stop()
 	err := testEnv.Stop()
 	Expect(err).ToNot(HaveOccurred())
 	Expect(err).ToNot(HaveOccurred())
 })
 })

+ 75 - 0
pkg/controllers/secretstore/util.go

@@ -0,0 +1,75 @@
+/*
+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 secretstore
+
+import (
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+)
+
+// NewSecretStoreCondition a set of default options for creating an External Secret Condition.
+func NewSecretStoreCondition(condType esapi.SecretStoreConditionType, status v1.ConditionStatus, reason, message string) *esapi.SecretStoreStatusCondition {
+	return &esapi.SecretStoreStatusCondition{
+		Type:               condType,
+		Status:             status,
+		LastTransitionTime: metav1.Now(),
+		Reason:             reason,
+		Message:            message,
+	}
+}
+
+// GetSecretStoreCondition returns the condition with the provided type.
+func GetSecretStoreCondition(status esapi.SecretStoreStatus, condType esapi.SecretStoreConditionType) *esapi.SecretStoreStatusCondition {
+	for i := range status.Conditions {
+		c := status.Conditions[i]
+		if c.Type == condType {
+			return &c
+		}
+	}
+	return nil
+}
+
+// SetExternalSecretCondition updates the external secret to include the provided
+// condition.
+func SetExternalSecretCondition(gs esapi.GenericStore, condition esapi.SecretStoreStatusCondition) {
+	status := gs.GetStatus()
+	currentCond := GetSecretStoreCondition(status, condition.Type)
+	if currentCond != nil && currentCond.Status == condition.Status &&
+		currentCond.Reason == condition.Reason && currentCond.Message == condition.Message {
+		return
+	}
+
+	// Do not update lastTransitionTime if the status of the condition doesn't change.
+	if currentCond != nil && currentCond.Status == condition.Status {
+		condition.LastTransitionTime = currentCond.LastTransitionTime
+	}
+
+	status.Conditions = append(filterOutCondition(status.Conditions, condition.Type), condition)
+	gs.SetStatus(status)
+}
+
+// filterOutCondition returns an empty set of conditions with the provided type.
+func filterOutCondition(conditions []esapi.SecretStoreStatusCondition, condType esapi.SecretStoreConditionType) []esapi.SecretStoreStatusCondition {
+	newConditions := make([]esapi.SecretStoreStatusCondition, 0, len(conditions))
+	for _, c := range conditions {
+		if c.Type == condType {
+			continue
+		}
+		newConditions = append(newConditions, c)
+	}
+	return newConditions
+}

+ 4 - 0
pkg/provider/akeyless/akeyless.go

@@ -103,6 +103,10 @@ func (a *Akeyless) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
 
 
+func (a *Akeyless) Validate() error {
+	return nil
+}
+
 // Implements store.Client.GetSecret Interface.
 // Implements store.Client.GetSecret Interface.
 // Retrieves a secret with the secret name defined in ref.Name.
 // Retrieves a secret with the secret name defined in ref.Name.
 func (a *Akeyless) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
 func (a *Akeyless) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {

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

@@ -186,6 +186,10 @@ func (kms *KeyManagementService) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
 
 
+func (kms *KeyManagementService) Validate() error {
+	return nil
+}
+
 func init() {
 func init() {
 	schema.Register(&KeyManagementService{}, &esv1alpha1.SecretStoreProvider{
 	schema.Register(&KeyManagementService{}, &esv1alpha1.SecretStoreProvider{
 		Alibaba: &esv1alpha1.AlibabaProvider{},
 		Alibaba: &esv1alpha1.AlibabaProvider{},

+ 9 - 2
pkg/provider/aws/parameterstore/parameterstore.go

@@ -19,7 +19,7 @@ import (
 	"fmt"
 	"fmt"
 
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/aws/client"
+	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/ssm"
 	"github.com/aws/aws-sdk-go/service/ssm"
 	"github.com/tidwall/gjson"
 	"github.com/tidwall/gjson"
 	ctrl "sigs.k8s.io/controller-runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -30,6 +30,7 @@ import (
 
 
 // ParameterStore is a provider for AWS ParameterStore.
 // ParameterStore is a provider for AWS ParameterStore.
 type ParameterStore struct {
 type ParameterStore struct {
+	sess   *session.Session
 	client PMInterface
 	client PMInterface
 }
 }
 
 
@@ -42,8 +43,9 @@ type PMInterface interface {
 var log = ctrl.Log.WithName("provider").WithName("aws").WithName("parameterstore")
 var log = ctrl.Log.WithName("provider").WithName("aws").WithName("parameterstore")
 
 
 // New constructs a ParameterStore Provider that is specific to a store.
 // New constructs a ParameterStore Provider that is specific to a store.
-func New(sess client.ConfigProvider) (*ParameterStore, error) {
+func New(sess *session.Session) (*ParameterStore, error) {
 	return &ParameterStore{
 	return &ParameterStore{
+		sess:   sess,
 		client: ssm.New(sess),
 		client: ssm.New(sess),
 	}, nil
 	}, nil
 }
 }
@@ -93,3 +95,8 @@ func (pm *ParameterStore) GetSecretMap(ctx context.Context, ref esv1alpha1.Exter
 func (pm *ParameterStore) Close(ctx context.Context) error {
 func (pm *ParameterStore) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
+
+func (pm *ParameterStore) Validate() error {
+	_, err := pm.sess.Config.Credentials.Get()
+	return err
+}

+ 2 - 0
pkg/provider/aws/provider.go

@@ -47,10 +47,12 @@ func newClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.C
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
+
 	sess, err := awsauth.New(ctx, store, kube, namespace, assumeRoler, awsauth.DefaultJWTProvider)
 	sess, err := awsauth.New(ctx, store, kube, namespace, assumeRoler, awsauth.DefaultJWTProvider)
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf(errUnableCreateSession, err)
 		return nil, fmt.Errorf(errUnableCreateSession, err)
 	}
 	}
+
 	switch prov.Service {
 	switch prov.Service {
 	case esv1alpha1.AWSServiceSecretsManager:
 	case esv1alpha1.AWSServiceSecretsManager:
 		return secretsmanager.New(sess)
 		return secretsmanager.New(sess)

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

@@ -16,6 +16,7 @@ package aws
 
 
 import (
 import (
 	"context"
 	"context"
+	"os"
 	"testing"
 	"testing"
 
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws"
@@ -32,6 +33,14 @@ func TestProvider(t *testing.T) {
 	cl := clientfake.NewClientBuilder().Build()
 	cl := clientfake.NewClientBuilder().Build()
 	p := Provider{}
 	p := Provider{}
 
 
+	// inject fake static credentials because we test
+	// if we are able to get credentials when constructing the client
+	// see #415
+	os.Setenv("AWS_ACCESS_KEY_ID", "1234")
+	os.Setenv("AWS_SECRET_ACCESS_KEY", "1234")
+	defer os.Unsetenv("AWS_ACCESS_KEY_ID")
+	defer os.Unsetenv("AWS_SECRET_ACCESS_KEY")
+
 	tbl := []struct {
 	tbl := []struct {
 		test    string
 		test    string
 		store   esv1alpha1.GenericStore
 		store   esv1alpha1.GenericStore

+ 9 - 2
pkg/provider/aws/secretsmanager/secretsmanager.go

@@ -19,7 +19,7 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
 
 
-	"github.com/aws/aws-sdk-go/aws/client"
+	"github.com/aws/aws-sdk-go/aws/session"
 	awssm "github.com/aws/aws-sdk-go/service/secretsmanager"
 	awssm "github.com/aws/aws-sdk-go/service/secretsmanager"
 	"github.com/tidwall/gjson"
 	"github.com/tidwall/gjson"
 	ctrl "sigs.k8s.io/controller-runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -30,6 +30,7 @@ import (
 
 
 // SecretsManager is a provider for AWS SecretsManager.
 // SecretsManager is a provider for AWS SecretsManager.
 type SecretsManager struct {
 type SecretsManager struct {
+	sess   *session.Session
 	client SMInterface
 	client SMInterface
 	cache  map[string]*awssm.GetSecretValueOutput
 	cache  map[string]*awssm.GetSecretValueOutput
 }
 }
@@ -43,8 +44,9 @@ type SMInterface interface {
 var log = ctrl.Log.WithName("provider").WithName("aws").WithName("secretsmanager")
 var log = ctrl.Log.WithName("provider").WithName("aws").WithName("secretsmanager")
 
 
 // New creates a new SecretsManager client.
 // New creates a new SecretsManager client.
-func New(sess client.ConfigProvider) (*SecretsManager, error) {
+func New(sess *session.Session) (*SecretsManager, error) {
 	return &SecretsManager{
 	return &SecretsManager{
+		sess:   sess,
 		client: awssm.New(sess),
 		client: awssm.New(sess),
 		cache:  make(map[string]*awssm.GetSecretValueOutput),
 		cache:  make(map[string]*awssm.GetSecretValueOutput),
 	}, nil
 	}, nil
@@ -132,3 +134,8 @@ func (sm *SecretsManager) GetSecretMap(ctx context.Context, ref esv1alpha1.Exter
 func (sm *SecretsManager) Close(ctx context.Context) error {
 func (sm *SecretsManager) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
+
+func (sm *SecretsManager) Validate() error {
+	_, err := sm.sess.Config.Credentials.Get()
+	return err
+}

+ 4 - 0
pkg/provider/azure/keyvault/keyvault.go

@@ -264,6 +264,10 @@ func (a *Azure) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
 
 
+func (a *Azure) Validate() error {
+	return nil
+}
+
 func getObjType(ref esv1alpha1.ExternalSecretDataRemoteRef) (string, string) {
 func getObjType(ref esv1alpha1.ExternalSecretDataRemoteRef) (string, string) {
 	objectType := defaultObjType
 	objectType := defaultObjType
 
 

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

@@ -89,6 +89,10 @@ func (p *Provider) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
 
 
+func (p *Provider) Validate() error {
+	return nil
+}
+
 func init() {
 func init() {
 	schema.Register(&Provider{}, &esv1alpha1.SecretStoreProvider{
 	schema.Register(&Provider{}, &esv1alpha1.SecretStoreProvider{
 		Fake: &esv1alpha1.FakeProvider{},
 		Fake: &esv1alpha1.FakeProvider{},

+ 11 - 0
pkg/provider/gcp/secretmanager/secretsmanager.go

@@ -40,6 +40,7 @@ const (
 	defaultVersion    = "latest"
 	defaultVersion    = "latest"
 
 
 	errGCPSMStore                             = "received invalid GCPSM SecretStore resource"
 	errGCPSMStore                             = "received invalid GCPSM SecretStore resource"
+	errUnableGetCredentials                   = "unable to get credentials: %w"
 	errClientClose                            = "unable to close SecretManager client: %w"
 	errClientClose                            = "unable to close SecretManager client: %w"
 	errMissingStoreSpec                       = "invalid: missing store spec"
 	errMissingStoreSpec                       = "invalid: missing store spec"
 	errInvalidClusterStoreMissingSAKNamespace = "invalid ClusterSecretStore: missing GCP SecretAccessKey Namespace"
 	errInvalidClusterStoreMissingSAKNamespace = "invalid ClusterSecretStore: missing GCP SecretAccessKey Namespace"
@@ -153,6 +154,12 @@ func (sm *ProviderGCP) NewClient(ctx context.Context, store esv1alpha1.GenericSt
 		return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
 		return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
 	}
 	}
 
 
+	// check if we can get credentials
+	_, err = ts.Token()
+	if err != nil {
+		return nil, fmt.Errorf(errUnableGetCredentials, err)
+	}
+
 	clientGCPSM, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
 	clientGCPSM, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
 		return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
@@ -238,6 +245,10 @@ func (sm *ProviderGCP) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
 
 
+func (sm *ProviderGCP) Validate() error {
+	return nil
+}
+
 func init() {
 func init() {
 	schema.Register(&ProviderGCP{}, &esv1alpha1.SecretStoreProvider{
 	schema.Register(&ProviderGCP{}, &esv1alpha1.SecretStoreProvider{
 		GCPSM: &esv1alpha1.GCPSMProvider{},
 		GCPSM: &esv1alpha1.GCPSMProvider{},

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

@@ -210,3 +210,7 @@ func (g *Gitlab) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecret
 func (g *Gitlab) Close(ctx context.Context) error {
 func (g *Gitlab) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
+
+func (g *Gitlab) Validate() error {
+	return nil
+}

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

@@ -313,6 +313,10 @@ func (ibm *providerIBM) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
 
 
+func (ibm *providerIBM) Validate() error {
+	return nil
+}
+
 func (ibm *providerIBM) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
 func (ibm *providerIBM) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
 	storeSpec := store.GetSpec()
 	storeSpec := store.GetSpec()
 	ibmSpec := storeSpec.Provider.IBM
 	ibmSpec := storeSpec.Provider.IBM

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

@@ -221,6 +221,10 @@ func (vms *VaultManagementService) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
 
 
+func (vms *VaultManagementService) Validate() error {
+	return nil
+}
+
 func init() {
 func init() {
 	schema.Register(&VaultManagementService{}, &esv1alpha1.SecretStoreProvider{
 	schema.Register(&VaultManagementService{}, &esv1alpha1.SecretStoreProvider{
 		Oracle: &esv1alpha1.OracleProvider{},
 		Oracle: &esv1alpha1.OracleProvider{},

+ 4 - 0
pkg/provider/provider.go

@@ -33,6 +33,10 @@ type SecretsClient interface {
 	// GetSecret returns a single secret from the provider
 	// GetSecret returns a single secret from the provider
 	GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error)
 	GetSecret(ctx context.Context, ref esv1alpha1.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 returns multiple k/v pairs from the provider
 	GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
 	GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
 	Close(ctx context.Context) error
 	Close(ctx context.Context) error

+ 4 - 0
pkg/provider/schema/schema_test.go

@@ -47,6 +47,10 @@ func (p *PP) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
 
 
+func (p *PP) Validate() error {
+	return nil
+}
+
 // TestRegister tests if the Register function
 // TestRegister tests if the Register function
 // (1) panics if it tries to register something invalid
 // (1) panics if it tries to register something invalid
 // (2) stores the correct provider.
 // (2) stores the correct provider.

+ 5 - 0
pkg/provider/testing/fake/fake.go

@@ -74,10 +74,15 @@ func (v *Client) WithGetSecret(secData []byte, err error) *Client {
 func (v *Client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 func (v *Client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 	return v.GetSecretMapFn(ctx, ref)
 	return v.GetSecretMapFn(ctx, ref)
 }
 }
+
 func (v *Client) Close(ctx context.Context) error {
 func (v *Client) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
 
 
+func (v *Client) Validate() error {
+	return nil
+}
+
 // WithGetSecretMap wraps the secret data map returned by this fake provider.
 // WithGetSecretMap wraps the secret data map returned by this fake provider.
 func (v *Client) WithGetSecretMap(secData map[string][]byte, err error) *Client {
 func (v *Client) WithGetSecretMap(secData map[string][]byte, err error) *Client {
 	v.GetSecretMapFn = func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 	v.GetSecretMapFn = func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {

+ 31 - 13
pkg/provider/vault/vault.go

@@ -46,19 +46,20 @@ var (
 const (
 const (
 	serviceAccTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
 	serviceAccTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
 
 
-	errVaultStore     = "received invalid Vault SecretStore resource: %w"
-	errVaultClient    = "cannot setup new vault client: %w"
-	errVaultCert      = "cannot set Vault CA certificate: %w"
-	errReadSecret     = "cannot read secret data from Vault: %w"
-	errAuthFormat     = "cannot initialize Vault client: no valid auth method specified: %w"
-	errDataField      = "failed to find data field"
-	errJSONUnmarshall = "failed to unmarshall JSON"
-	errSecretFormat   = "secret data not in expected format"
-	errVaultToken     = "cannot parse Vault authentication token: %w"
-	errVaultReqParams = "cannot set Vault request parameters: %w"
-	errVaultRequest   = "error from Vault request: %w"
-	errVaultResponse  = "cannot parse Vault response: %w"
-	errServiceAccount = "cannot read Kubernetes service account token from file system: %w"
+	errVaultStore         = "received invalid Vault SecretStore resource: %w"
+	errVaultClient        = "cannot setup new vault client: %w"
+	errVaultCert          = "cannot set Vault CA certificate: %w"
+	errReadSecret         = "cannot read secret data from Vault: %w"
+	errAuthFormat         = "cannot initialize Vault client: no valid auth method specified"
+	errInvalidCredentials = "invalid vault credentials: %w"
+	errDataField          = "failed to find data field"
+	errJSONUnmarshall     = "failed to unmarshall JSON"
+	errSecretFormat       = "secret data not in expected format"
+	errVaultToken         = "cannot parse Vault authentication token: %w"
+	errVaultReqParams     = "cannot set Vault request parameters: %w"
+	errVaultRequest       = "error from Vault request: %w"
+	errVaultResponse      = "cannot parse Vault response: %w"
+	errServiceAccount     = "cannot read Kubernetes service account token from file system: %w"
 
 
 	errGetKubeSA        = "cannot get Kubernetes service account %q: %w"
 	errGetKubeSA        = "cannot get Kubernetes service account %q: %w"
 	errGetKubeSASecrets = "cannot find secrets bound to service account: %q"
 	errGetKubeSASecrets = "cannot find secrets bound to service account: %q"
@@ -149,6 +150,7 @@ func (c *connector) NewClient(ctx context.Context, store esv1alpha1.GenericStore
 	}
 	}
 
 
 	vStore.client = client
 	vStore.client = client
+
 	return vStore, nil
 	return vStore, nil
 }
 }
 
 
@@ -181,6 +183,14 @@ func (v *client) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
 
 
+func (v *client) Validate() error {
+	err := checkToken(context.Background(), v)
+	if err != nil {
+		return fmt.Errorf(errInvalidCredentials, err)
+	}
+	return nil
+}
+
 func (v *client) buildPath(path string) string {
 func (v *client) buildPath(path string) string {
 	optionalMount := v.store.Path
 	optionalMount := v.store.Path
 	origPath := strings.Split(path, "/")
 	origPath := strings.Split(path, "/")
@@ -528,6 +538,14 @@ func (v *client) secretKeyRef(ctx context.Context, secretRef *esmeta.SecretKeySe
 	return valueStr, nil
 	return valueStr, nil
 }
 }
 
 
+// checkToken does a lookup and checks if the provided token exists.
+func checkToken(ctx context.Context, vStore *client) error {
+	// https://www.vaultproject.io/api-docs/auth/token#lookup-a-token-self
+	req := vStore.client.NewRequest("GET", "/v1/auth/token/lookup-self")
+	_, err := vStore.client.RawRequestWithContext(ctx, req)
+	return err
+}
+
 // appRoleParameters creates the required body for Vault AppRole Auth.
 // appRoleParameters creates the required body for Vault AppRole Auth.
 // Reference - https://www.vaultproject.io/api-docs/auth/approle#login-with-approle
 // Reference - https://www.vaultproject.io/api-docs/auth/approle#login-with-approle
 func appRoleParameters(role, secret string) map[string]string {
 func appRoleParameters(role, secret string) map[string]string {

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

@@ -377,6 +377,10 @@ func (w *WebHook) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
 
 
+func (w *WebHook) Validate() error {
+	return nil
+}
+
 func executeTemplateString(tmpl string, data map[string]map[string]string) (string, error) {
 func executeTemplateString(tmpl string, data map[string]map[string]string) (string, error) {
 	result, err := executeTemplate(tmpl, data)
 	result, err := executeTemplate(tmpl, data)
 	if err != nil {
 	if err != nil {

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

@@ -276,6 +276,10 @@ func (c *lockboxSecretsClient) Close(ctx context.Context) error {
 	return nil
 	return nil
 }
 }
 
 
+func (c *lockboxSecretsClient) Validate() error {
+	return nil
+}
+
 func getValueAsIs(entry *lockbox.Payload_Entry) (interface{}, error) {
 func getValueAsIs(entry *lockbox.Payload_Entry) (interface{}, error) {
 	switch entry.Value.(type) {
 	switch entry.Value.(type) {
 	case *lockbox.Payload_Entry_TextValue:
 	case *lockbox.Payload_Entry_TextValue: