Explorar el Código

Merge branch 'main' into feat/immutable-secrets

Arthur hace 4 años
padre
commit
9f2a17f220
Se han modificado 96 ficheros con 5372 adiciones y 278 borrados
  1. 1 1
      .github/workflows/ci.yml
  2. 1 1
      .github/workflows/e2e.yml
  3. 1 1
      .github/workflows/ok-to-test.yml
  4. 1 1
      Dockerfile
  5. 9 0
      README.md
  6. 5 0
      apis/externalsecrets/v1alpha1/externalsecret_types.go
  7. 41 0
      apis/externalsecrets/v1alpha1/secretstore_alibaba_types.go
  8. 2 1
      apis/externalsecrets/v1alpha1/secretstore_gcpsm_types.go
  9. 40 0
      apis/externalsecrets/v1alpha1/secretstore_gitlab_types.go
  10. 46 0
      apis/externalsecrets/v1alpha1/secretstore_oracle_types.go
  11. 16 0
      apis/externalsecrets/v1alpha1/secretstore_types.go
  12. 35 0
      apis/externalsecrets/v1alpha1/secretstore_yandexlockbox_types.go
  13. 202 0
      apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go
  14. 1 1
      apis/meta/v1/types.go
  15. 2 2
      deploy/charts/external-secrets/Chart.yaml
  16. 3 1
      deploy/charts/external-secrets/README.md
  17. 8 1
      deploy/charts/external-secrets/templates/deployment.yaml
  18. 1 0
      deploy/charts/external-secrets/templates/serviceaccount.yaml
  19. 7 0
      deploy/charts/external-secrets/values.yaml
  20. 213 26
      deploy/crds/external-secrets.io_clustersecretstores.yaml
  21. 213 26
      deploy/crds/external-secrets.io_secretstores.yaml
  22. 31 8
      docs/contributing-devguide.md
  23. BIN
      docs/pictures/screenshot_API_key.png
  24. BIN
      docs/pictures/screenshot_fingerprint.png
  25. BIN
      docs/pictures/screenshot_gitlab_projectID.png
  26. BIN
      docs/pictures/screenshot_gitlab_token.png
  27. BIN
      docs/pictures/screenshot_gitlab_token_created.png
  28. BIN
      docs/pictures/screenshot_region.png
  29. BIN
      docs/pictures/screenshot_tenancy_OCID.png
  30. BIN
      docs/pictures/screenshot_user_OCID.png
  31. 1 1
      docs/provider-aws-parameter-store.md
  32. 54 0
      docs/provider-gitlab-project-variables.md
  33. 61 5
      docs/provider-google-secrets-manager.md
  34. 54 0
      docs/provider-oracle-vault.md
  35. 86 0
      docs/provider-yandex-lockbox.md
  36. 9 0
      docs/snippets/gitlab-credentials-secret.yaml
  37. 18 0
      docs/snippets/gitlab-external-secret-json.yaml
  38. 19 0
      docs/snippets/gitlab-external-secret.yaml
  39. 15 0
      docs/snippets/gitlab-secret-store.yaml
  40. 10 0
      docs/snippets/oracle-credentials-secret.yaml
  41. 16 0
      docs/snippets/oracle-external-secret.yaml
  42. 18 0
      docs/snippets/oracle-secret-store.yaml
  43. 3 0
      docs/snippets/provider-aws-access.md
  44. 2 1
      docs/snippets/vault-approle-store.yaml
  45. 219 1
      docs/spec.md
  46. BIN
      e2e/.DS_Store
  47. 3 0
      e2e/e2e_test.go
  48. 11 0
      e2e/framework/addon/eso.go
  49. 9 0
      e2e/framework/eso.go
  50. 12 0
      e2e/k8s/eso.scoped.values.yaml
  51. 7 0
      e2e/run.sh
  52. 47 0
      e2e/suite/alibaba/alibaba.go
  53. 118 0
      e2e/suite/alibaba/provider.go
  54. 1 0
      e2e/suite/aws/provider.go
  55. 1 0
      e2e/suite/aws/secretsmanager.go
  56. 45 0
      e2e/suite/gitlab/gitlab.go
  57. 131 0
      e2e/suite/gitlab/provider.go
  58. 47 0
      e2e/suite/oracle/oracle.go
  59. 124 0
      e2e/suite/oracle/provider.go
  60. 12 6
      go.mod
  61. 35 5
      go.sum
  62. 6 0
      hack/api-docs/mkdocs.yml
  63. 4 1
      main.go
  64. 42 102
      pkg/controllers/externalsecret/externalsecret_controller.go
  65. 157 0
      pkg/controllers/externalsecret/externalsecret_controller_template.go
  66. 268 51
      pkg/controllers/externalsecret/externalsecret_controller_test.go
  67. 35 0
      pkg/provider/alibaba/fake/fake.go
  68. 193 0
      pkg/provider/alibaba/kms.go
  69. 197 0
      pkg/provider/alibaba/kms_test.go
  70. 1 1
      pkg/provider/aws/parameterstore/parameterstore.go
  71. 27 3
      pkg/provider/aws/secretsmanager/fake/fake.go
  72. 23 3
      pkg/provider/aws/secretsmanager/secretsmanager.go
  73. 75 4
      pkg/provider/aws/secretsmanager/secretsmanager_test.go
  74. 1 1
      pkg/provider/azure/keyvault/keyvault.go
  75. 1 1
      pkg/provider/fake/fake.go
  76. 24 12
      pkg/provider/gcp/secretmanager/secretsmanager.go
  77. 39 0
      pkg/provider/gitlab/fake/fake.go
  78. 212 0
      pkg/provider/gitlab/gitlab.go
  79. 178 0
      pkg/provider/gitlab/gitlab_test.go
  80. 1 1
      pkg/provider/ibm/provider.go
  81. 36 0
      pkg/provider/oracle/fake/fake.go
  82. 213 0
      pkg/provider/oracle/oracle.go
  83. 173 0
      pkg/provider/oracle/oracle_test.go
  84. 1 1
      pkg/provider/provider.go
  85. 4 0
      pkg/provider/register/register.go
  86. 1 1
      pkg/provider/schema/schema.go
  87. 1 1
      pkg/provider/schema/schema_test.go
  88. 1 1
      pkg/provider/vault/vault.go
  89. 39 0
      pkg/provider/yandex/lockbox/client/client.go
  90. 151 0
      pkg/provider/yandex/lockbox/client/fake/fake.go
  91. 144 0
      pkg/provider/yandex/lockbox/client/grpc/grpc.go
  92. 299 0
      pkg/provider/yandex/lockbox/lockbox.go
  93. 677 0
      pkg/provider/yandex/lockbox/lockbox_test.go
  94. 18 5
      pkg/utils/utils.go
  95. 62 0
      pkg/utils/utils_test.go
  96. 1 0
      tools.go

+ 1 - 1
.github/workflows/ci.yml

@@ -176,7 +176,7 @@ jobs:
           make test
 
       - name: Publish Unit Test Coverage
-        uses: codecov/codecov-action@v2.0.2
+        uses: codecov/codecov-action@v2.0.3
         with:
           flags: unittests
           file: ./cover.out

+ 1 - 1
.github/workflows/e2e.yml

@@ -151,7 +151,7 @@ jobs:
         make test.e2e
 
     # Update check run called "integration-fork"
-    - uses: actions/github-script@v1
+    - uses: actions/github-script@v4.1
       id: update-check-run
       if: ${{ always() }}
       env:

+ 1 - 1
.github/workflows/ok-to-test.yml

@@ -23,7 +23,7 @@ jobs:
         private_key: ${{ secrets.PRIVATE_KEY }}
 
     - name: Slash Command Dispatch
-      uses: peter-evans/slash-command-dispatch@v2.2.1
+      uses: peter-evans/slash-command-dispatch@v2.3.0
       env:
         TOKEN: ${{ steps.generate_token.outputs.token }}
       with:

+ 1 - 1
Dockerfile

@@ -1,4 +1,4 @@
-FROM alpine:3.14.0
+FROM alpine:3.14.2
 ARG TARGETOS
 ARG TARGETARCH
 COPY bin/external-secrets-${TARGETOS}-${TARGETARCH} /bin/external-secrets

+ 9 - 0
README.md

@@ -17,6 +17,10 @@ Multiple people and organizations are joining efforts to create a single Externa
 - [Google Cloud Secrets Manager](https://external-secrets.io/provider-google-secrets-manager/)
 - [Azure Key Vault](https://external-secrets.io/provider-azure-key-vault/)
 - [IBM Cloud Secrets Manager](https://external-secrets.io/provider-ibm-secrets-manager/)
+- [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/)
+- [Gitlab Project Variables](https://external-secrets.io/provider-gitlab-project-variables/)
+- [Alibaba Cloud KMS](https://www.alibabacloud.com/product/kms) (Docs still missing, PRs welcomed!)
+- [Oracle Vault]( https://external-secrets.io/provider-oracle-vault) 
 
 ## Stability and Support Level
 
@@ -35,6 +39,11 @@ Multiple people and organizations are joining efforts to create a single Externa
 | ------------------------------------------------------------------- | :-------: | :----------------------------------------: |
 | [Azure KV](https://external-secrets.io/provider-azure-key-vault/)   |   alpha   | @ahmedmus-1A @asnowfix @ncourbet-1A @1A-mj |
 | [IBM SM](https://external-secrets.io/provider-ibm-secrets-manager/) |   alpha   |   @knelasevero @sebagomez @ricardoptcosta  |
+| [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/) |   alpha   |   @AndreyZamyslov @knelasevero          |
+| [Gitlab Project Variables](https://external-secrets.io/provider-gitlab-project-variables/) |   alpha   |   @Jabray5          |
+| Alibaba Cloud KMS                                                   |   alpha  | @ElsaChelala                                |
+| [Oracle Vault]( https://external-secrets.io/provider-oracle-vault)  |   alpha  | @KianTigger                                 |
+
 
 ## Documentation
 

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

@@ -211,6 +211,11 @@ type ExternalSecret struct {
 	Status ExternalSecretStatus `json:"status,omitempty"`
 }
 
+const (
+	// AnnotationDataHash is used to ensure consistency.
+	AnnotationDataHash = "reconcile.external-secrets.io/data-hash"
+)
+
 // +kubebuilder:object:root=true
 
 // ExternalSecretList contains a list of ExternalSecret resources.

+ 41 - 0
apis/externalsecrets/v1alpha1/secretstore_alibaba_types.go

@@ -0,0 +1,41 @@
+/*
+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 v1alpha1
+
+import (
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// AlibabaAuth contains a secretRef for credentials.
+type AlibabaAuth struct {
+	SecretRef AlibabaAuthSecretRef `json:"secretRef"`
+}
+
+// AlibabaAuthSecretRef holds secret references for Alibaba credentials.
+type AlibabaAuthSecretRef struct {
+	// The AccessKeyID is used for authentication
+	AccessKeyID esmeta.SecretKeySelector `json:"accessKeyIDSecretRef"`
+	// The AccessKeySecret is used for authentication
+	AccessKeySecret esmeta.SecretKeySelector `json:"accessKeySecretSecretRef"`
+}
+
+// AlibabaProvider configures a store to sync secrets using the Alibaba Secret Manager provider.
+type AlibabaProvider struct {
+	Auth *AlibabaAuth `json:"auth"`
+	// +optional
+	Endpoint string `json:"endpoint"`
+	// Alibaba Region to be used for the provider
+	RegionID string `json:"regionID"`
+}

+ 2 - 1
apis/externalsecrets/v1alpha1/secretstore_gcpsm_types.go

@@ -31,7 +31,8 @@ type GCPSMAuthSecretRef struct {
 // GCPSMProvider Configures a store to sync secrets using the GCP Secret Manager provider.
 type GCPSMProvider struct {
 	// Auth defines the information necessary to authenticate against GCP
-	Auth GCPSMAuth `json:"auth"`
+	// +optional
+	Auth GCPSMAuth `json:"auth,omitempty"`
 
 	// ProjectID project where secret is located
 	ProjectID string `json:"projectID,omitempty"`

+ 40 - 0
apis/externalsecrets/v1alpha1/secretstore_gitlab_types.go

@@ -0,0 +1,40 @@
+/*
+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 v1alpha1
+
+import (
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// Configures a store to sync secrets with a GitLab instance.
+type GitlabProvider struct {
+	// URL configures the GitLab instance URL. Defaults to https://gitlab.com/.
+	URL string `json:"url,omitempty"`
+
+	// Auth configures how secret-manager authenticates with a GitLab instance.
+	Auth GitlabAuth `json:"auth"`
+
+	// ProjectID specifies a project where secrets are located.
+	ProjectID string `json:"projectID,omitempty"`
+}
+
+type GitlabAuth struct {
+	SecretRef GitlabSecretRef `json:"SecretRef"`
+}
+
+type GitlabSecretRef struct {
+	// AccessToken is used for authentication.
+	AccessToken esmeta.SecretKeySelector `json:"accessToken,omitempty"`
+}

+ 46 - 0
apis/externalsecrets/v1alpha1/secretstore_oracle_types.go

@@ -0,0 +1,46 @@
+/*
+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 v1alpha1
+
+import (
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// Configures an store to sync secrets using a Oracle Vault
+// backend.
+type OracleProvider struct {
+	// Auth configures how secret-manager authenticates with the Oracle Vault.
+	Auth OracleAuth `json:"auth"`
+
+	// User is an access OCID specific to the account.
+	User string `json:"user,omitempty"`
+
+	// projectID is an access token specific to the secret.
+	Tenancy string `json:"tenancy,omitempty"`
+
+	// projectID is an access token specific to the secret.
+	Region string `json:"region,omitempty"`
+}
+
+type OracleAuth struct {
+	// SecretRef to pass through sensitive information.
+	SecretRef OracleSecretRef `json:"secretRef"`
+}
+
+type OracleSecretRef struct {
+	// The Access Token is used for authentication
+	PrivateKey esmeta.SecretKeySelector `json:"privatekey,omitempty"`
+
+	// projectID is an access token specific to the secret.
+	Fingerprint esmeta.SecretKeySelector `json:"fingerprint,omitempty"`
+}

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

@@ -50,9 +50,25 @@ type SecretStoreProvider struct {
 	// +optional
 	GCPSM *GCPSMProvider `json:"gcpsm,omitempty"`
 
+	// Oracle configures this store to sync secrets using Oracle Vault provider
+	// +optional
+	Oracle *OracleProvider `json:"oracle,omitempty"`
+
 	// IBM configures this store to sync secrets using IBM Cloud provider
 	// +optional
 	IBM *IBMProvider `json:"ibm,omitempty"`
+
+	// YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
+	// +optional
+	YandexLockbox *YandexLockboxProvider `json:"yandexlockbox,omitempty"`
+
+	// GItlab configures this store to sync secrets using Gitlab Variables provider
+	// +optional
+	Gitlab *GitlabProvider `json:"gitlab,omitempty"`
+
+	// Alibaba configures this store to sync secrets using Alibaba Cloud provider
+	// +optional
+	Alibaba *AlibabaProvider `json:"alibaba,omitempty"`
 }
 
 type SecretStoreConditionType string

+ 35 - 0
apis/externalsecrets/v1alpha1/secretstore_yandexlockbox_types.go

@@ -0,0 +1,35 @@
+/*
+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 v1alpha1
+
+import (
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+type YandexLockboxAuth struct {
+	// The authorized key used for authentication
+	// +optional
+	AuthorizedKey esmeta.SecretKeySelector `json:"authorizedKeySecretRef,omitempty"`
+}
+
+// YandexLockboxProvider Configures a store to sync secrets using the Yandex Lockbox provider.
+type YandexLockboxProvider struct {
+	// Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
+	// +optional
+	APIEndpoint string `json:"apiEndpoint,omitempty"`
+
+	// Auth defines the information necessary to authenticate against Yandex Lockbox
+	Auth YandexLockboxAuth `json:"auth"`
+}

+ 202 - 0
apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go

@@ -102,6 +102,59 @@ func (in *AWSProvider) DeepCopy() *AWSProvider {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AlibabaAuth) DeepCopyInto(out *AlibabaAuth) {
+	*out = *in
+	in.SecretRef.DeepCopyInto(&out.SecretRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlibabaAuth.
+func (in *AlibabaAuth) DeepCopy() *AlibabaAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(AlibabaAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AlibabaAuthSecretRef) DeepCopyInto(out *AlibabaAuthSecretRef) {
+	*out = *in
+	in.AccessKeyID.DeepCopyInto(&out.AccessKeyID)
+	in.AccessKeySecret.DeepCopyInto(&out.AccessKeySecret)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlibabaAuthSecretRef.
+func (in *AlibabaAuthSecretRef) DeepCopy() *AlibabaAuthSecretRef {
+	if in == nil {
+		return nil
+	}
+	out := new(AlibabaAuthSecretRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AlibabaProvider) DeepCopyInto(out *AlibabaProvider) {
+	*out = *in
+	if in.Auth != nil {
+		in, out := &in.Auth, &out.Auth
+		*out = new(AlibabaAuth)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlibabaProvider.
+func (in *AlibabaProvider) DeepCopy() *AlibabaProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(AlibabaProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *AzureKVAuth) DeepCopyInto(out *AzureKVAuth) {
 	*out = *in
@@ -504,6 +557,54 @@ func (in *GCPSMProvider) DeepCopy() *GCPSMProvider {
 	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
+	in.SecretRef.DeepCopyInto(&out.SecretRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabAuth.
+func (in *GitlabAuth) DeepCopy() *GitlabAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(GitlabAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GitlabProvider) DeepCopyInto(out *GitlabProvider) {
+	*out = *in
+	in.Auth.DeepCopyInto(&out.Auth)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabProvider.
+func (in *GitlabProvider) DeepCopy() *GitlabProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(GitlabProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GitlabSecretRef) DeepCopyInto(out *GitlabSecretRef) {
+	*out = *in
+	in.AccessToken.DeepCopyInto(&out.AccessToken)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabSecretRef.
+func (in *GitlabSecretRef) DeepCopy() *GitlabSecretRef {
+	if in == nil {
+		return nil
+	}
+	out := new(GitlabSecretRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *IBMAuth) DeepCopyInto(out *IBMAuth) {
 	*out = *in
@@ -557,6 +658,55 @@ func (in *IBMProvider) DeepCopy() *IBMProvider {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OracleAuth) DeepCopyInto(out *OracleAuth) {
+	*out = *in
+	in.SecretRef.DeepCopyInto(&out.SecretRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OracleAuth.
+func (in *OracleAuth) DeepCopy() *OracleAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(OracleAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OracleProvider) DeepCopyInto(out *OracleProvider) {
+	*out = *in
+	in.Auth.DeepCopyInto(&out.Auth)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OracleProvider.
+func (in *OracleProvider) DeepCopy() *OracleProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(OracleProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OracleSecretRef) DeepCopyInto(out *OracleSecretRef) {
+	*out = *in
+	in.PrivateKey.DeepCopyInto(&out.PrivateKey)
+	in.Fingerprint.DeepCopyInto(&out.Fingerprint)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OracleSecretRef.
+func (in *OracleSecretRef) DeepCopy() *OracleSecretRef {
+	if in == nil {
+		return nil
+	}
+	out := new(OracleSecretRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SecretStore) DeepCopyInto(out *SecretStore) {
 	*out = *in
@@ -639,11 +789,31 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(GCPSMProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Oracle != nil {
+		in, out := &in.Oracle, &out.Oracle
+		*out = new(OracleProvider)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.IBM != nil {
 		in, out := &in.IBM, &out.IBM
 		*out = new(IBMProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.YandexLockbox != nil {
+		in, out := &in.YandexLockbox, &out.YandexLockbox
+		*out = new(YandexLockboxProvider)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Gitlab != nil {
+		in, out := &in.Gitlab, &out.Gitlab
+		*out = new(GitlabProvider)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Alibaba != nil {
+		in, out := &in.Alibaba, &out.Alibaba
+		*out = new(AlibabaProvider)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
@@ -949,3 +1119,35 @@ func (in *VaultProvider) DeepCopy() *VaultProvider {
 	in.DeepCopyInto(out)
 	return out
 }
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *YandexLockboxAuth) DeepCopyInto(out *YandexLockboxAuth) {
+	*out = *in
+	in.AuthorizedKey.DeepCopyInto(&out.AuthorizedKey)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexLockboxAuth.
+func (in *YandexLockboxAuth) DeepCopy() *YandexLockboxAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(YandexLockboxAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *YandexLockboxProvider) DeepCopyInto(out *YandexLockboxProvider) {
+	*out = *in
+	in.Auth.DeepCopyInto(&out.Auth)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexLockboxProvider.
+func (in *YandexLockboxProvider) DeepCopy() *YandexLockboxProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(YandexLockboxProvider)
+	in.DeepCopyInto(out)
+	return out
+}

+ 1 - 1
apis/meta/v1/types.go

@@ -18,7 +18,7 @@ package v1
 // In some instances, `key` is a required field.
 type SecretKeySelector struct {
 	// The name of the Secret resource being referred to.
-	Name string `json:"name"`
+	Name string `json:"name,omitempty"`
 	// Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
 	// to the namespace of the referent.
 	// +optional

+ 2 - 2
deploy/charts/external-secrets/Chart.yaml

@@ -2,8 +2,8 @@ apiVersion: v2
 name: external-secrets
 description: External secret management for Kubernetes
 type: application
-version: "0.3.3"
-appVersion: "v0.3.3"
+version: "0.3.5"
+appVersion: "v0.3.5"
 kubeVersion: ">= 1.11.0-0"
 keywords:
   - kubernetes-external-secrets

+ 3 - 1
deploy/charts/external-secrets/README.md

@@ -4,7 +4,7 @@
 
 [//]: # (README.md generated by gotmpl. DO NOT EDIT.)
 
-![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 0.3.3](https://img.shields.io/badge/Version-0.3.3-informational?style=flat-square)
+![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 0.3.5](https://img.shields.io/badge/Version-0.3.5-informational?style=flat-square)
 
 External secret management for Kubernetes
 
@@ -49,11 +49,13 @@ The command removes all the Kubernetes components associated with the chart and
 | podAnnotations | object | `{}` |  |
 | podLabels | object | `{}` |  |
 | podSecurityContext | object | `{}` |  |
+| priorityClassName | string | `""` | Pod priority class name. |
 | prometheus.enabled | bool | `false` | Specifies whether to expose Service resource for collecting Prometheus metrics |
 | prometheus.service.port | int | `8080` |  |
 | rbac.create | bool | `true` | Specifies whether role and rolebinding resources should be created. |
 | replicaCount | int | `1` |  |
 | resources | object | `{}` |  |
+| scopedNamespace | string | `""` | If set external secrets are only reconciled in the provided namespace |
 | securityContext | object | `{}` |  |
 | serviceAccount.annotations | object | `{}` | Annotations to add to the service account. |
 | serviceAccount.create | bool | `true` | Specifies whether a service account should be created. |

+ 8 - 1
deploy/charts/external-secrets/templates/deployment.yaml

@@ -2,6 +2,7 @@ apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: {{ include "external-secrets.fullname" . }}
+  namespace: {{ .Release.Namespace | quote }}
   labels:
     {{- include "external-secrets.labels" . | nindent 4 }}
 spec:
@@ -38,11 +39,14 @@ spec:
           {{- end }}
           image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
           imagePullPolicy: {{ .Values.image.pullPolicy }}
-          {{- if or (.Values.leaderElect) (.Values.extraArgs) }}
+          {{- if or (.Values.leaderElect) (.Values.scopedNamespace) (.Values.extraArgs) }}
           args:
           {{- if .Values.leaderElect }}
           - --enable-leader-election=true
           {{- end }}
+          {{- if .Values.scopedNamespace }}
+          - --namespace={{ .Values.scopedNamespace }}
+          {{- end }}
           {{- range $key, $value := .Values.extraArgs }}
             {{- if $value }}
           - --{{ $key }}={{ $value }}
@@ -74,3 +78,6 @@ spec:
       tolerations:
         {{- toYaml . | nindent 8 }}
       {{- end }}
+      {{- if .Values.priorityClassName }}
+      priorityClassName: {{ .Values.priorityClassName }}
+      {{- end }}

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

@@ -3,6 +3,7 @@ apiVersion: v1
 kind: ServiceAccount
 metadata:
   name: {{ include "external-secrets.serviceAccountName" . }}
+  namespace: {{ .Release.Namespace | quote }}
   labels:
     {{- include "external-secrets.labels" . | nindent 4 }}
   {{- with .Values.serviceAccount.annotations }}

+ 7 - 0
deploy/charts/external-secrets/values.yaml

@@ -17,6 +17,10 @@ fullnameOverride: ""
 # than one instance of external-secrets operates at a time.
 leaderElect: false
 
+# -- If set external secrets are only reconciled in the
+# provided namespace
+scopedNamespace: ""
+
 serviceAccount:
   # -- Specifies whether a service account should be created.
   create: true
@@ -66,3 +70,6 @@ nodeSelector: {}
 tolerations: []
 
 affinity: {}
+
+# -- Pod priority class name.
+priorityClassName: ""

+ 213 - 26
deploy/crds/external-secrets.io_clustersecretstores.yaml

@@ -54,6 +54,73 @@ spec:
                 maxProperties: 1
                 minProperties: 1
                 properties:
+                  alibaba:
+                    description: Alibaba configures this store to sync secrets using
+                      Alibaba Cloud provider
+                    properties:
+                      auth:
+                        description: AlibabaAuth contains a secretRef for credentials.
+                        properties:
+                          secretRef:
+                            description: AlibabaAuthSecretRef holds secret references
+                              for Alibaba credentials.
+                            properties:
+                              accessKeyIDSecretRef:
+                                description: The AccessKeyID is used for authentication
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                              accessKeySecretSecretRef:
+                                description: The AccessKeySecret is used for authentication
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                            required:
+                            - accessKeyIDSecretRef
+                            - accessKeySecretSecretRef
+                            type: object
+                        required:
+                        - secretRef
+                        type: object
+                      endpoint:
+                        type: string
+                      regionID:
+                        description: Alibaba Region to be used for the provider
+                        type: string
+                    required:
+                    - auth
+                    - regionID
+                    type: object
                   aws:
                     description: AWS configures this store to sync secrets using AWS
                       Secret Manager provider
@@ -108,8 +175,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                               secretAccessKeySecretRef:
                                 description: The SecretAccessKey is used for authentication
@@ -130,8 +195,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                             type: object
                         type: object
@@ -179,8 +242,6 @@ spec:
                                   to. Ignored if referent is not cluster-scoped. cluster-scoped
                                   defaults to the namespace of the referent.
                                 type: string
-                            required:
-                            - name
                             type: object
                           clientSecret:
                             description: The Azure ClientSecret of the service principle
@@ -200,8 +261,6 @@ spec:
                                   to. Ignored if referent is not cluster-scoped. cluster-scoped
                                   defaults to the namespace of the referent.
                                 type: string
-                            required:
-                            - name
                             type: object
                         required:
                         - clientId
@@ -249,8 +308,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                             type: object
                         required:
@@ -259,6 +316,49 @@ spec:
                       projectID:
                         description: ProjectID project where secret is located
                         type: string
+                    type: object
+                  gitlab:
+                    description: GItlab configures this store to sync secrets using
+                      Gitlab Variables provider
+                    properties:
+                      auth:
+                        description: Auth configures how secret-manager authenticates
+                          with a GitLab instance.
+                        properties:
+                          SecretRef:
+                            properties:
+                              accessToken:
+                                description: AccessToken is used for authentication.
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                            type: object
+                        required:
+                        - SecretRef
+                        type: object
+                      projectID:
+                        description: ProjectID specifies a project where secrets are
+                          located.
+                        type: string
+                      url:
+                        description: URL configures the GitLab instance URL. Defaults
+                          to https://gitlab.com/.
+                        type: string
                     required:
                     - auth
                     type: object
@@ -291,8 +391,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                             type: object
                         required:
@@ -305,6 +403,76 @@ spec:
                     required:
                     - auth
                     type: object
+                  oracle:
+                    description: Oracle configures this store to sync secrets using
+                      Oracle Vault provider
+                    properties:
+                      auth:
+                        description: Auth configures how secret-manager authenticates
+                          with the Oracle Vault.
+                        properties:
+                          secretRef:
+                            description: SecretRef to pass through sensitive information.
+                            properties:
+                              fingerprint:
+                                description: projectID is an access token specific
+                                  to the secret.
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                              privatekey:
+                                description: The Access Token is used for authentication
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                            type: object
+                        required:
+                        - secretRef
+                        type: object
+                      region:
+                        description: projectID is an access token specific to the
+                          secret.
+                        type: string
+                      tenancy:
+                        description: projectID is an access token specific to the
+                          secret.
+                        type: string
+                      user:
+                        description: User is an access OCID specific to the account.
+                        type: string
+                    required:
+                    - auth
+                    type: object
                   vault:
                     description: Vault configures this store to sync secrets using
                       Hashi provider
@@ -351,8 +519,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                             required:
                             - path
@@ -384,8 +550,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                               secretRef:
                                 description: SecretRef to a key in a Secret resource
@@ -408,8 +572,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                             type: object
                           jwt:
@@ -441,8 +603,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                             type: object
                           kubernetes:
@@ -483,8 +643,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                               serviceAccountRef:
                                 description: Optional service account field containing
@@ -537,8 +695,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                               username:
                                 description: Username is a LDAP user name used to
@@ -566,8 +722,6 @@ spec:
                                   to. Ignored if referent is not cluster-scoped. cluster-scoped
                                   defaults to the namespace of the referent.
                                 type: string
-                            required:
-                            - name
                             type: object
                         type: object
                       caBundle:
@@ -608,6 +762,39 @@ spec:
                     - path
                     - server
                     type: object
+                  yandexlockbox:
+                    description: YandexLockbox configures this store to sync secrets
+                      using Yandex Lockbox provider
+                    properties:
+                      apiEndpoint:
+                        description: Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
+                        type: string
+                      auth:
+                        description: Auth defines the information necessary to authenticate
+                          against Yandex Lockbox
+                        properties:
+                          authorizedKeySecretRef:
+                            description: The authorized key used for authentication
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                        type: object
+                    required:
+                    - auth
+                    type: object
                 type: object
             required:
             - provider

+ 213 - 26
deploy/crds/external-secrets.io_secretstores.yaml

@@ -54,6 +54,73 @@ spec:
                 maxProperties: 1
                 minProperties: 1
                 properties:
+                  alibaba:
+                    description: Alibaba configures this store to sync secrets using
+                      Alibaba Cloud provider
+                    properties:
+                      auth:
+                        description: AlibabaAuth contains a secretRef for credentials.
+                        properties:
+                          secretRef:
+                            description: AlibabaAuthSecretRef holds secret references
+                              for Alibaba credentials.
+                            properties:
+                              accessKeyIDSecretRef:
+                                description: The AccessKeyID is used for authentication
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                              accessKeySecretSecretRef:
+                                description: The AccessKeySecret is used for authentication
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                            required:
+                            - accessKeyIDSecretRef
+                            - accessKeySecretSecretRef
+                            type: object
+                        required:
+                        - secretRef
+                        type: object
+                      endpoint:
+                        type: string
+                      regionID:
+                        description: Alibaba Region to be used for the provider
+                        type: string
+                    required:
+                    - auth
+                    - regionID
+                    type: object
                   aws:
                     description: AWS configures this store to sync secrets using AWS
                       Secret Manager provider
@@ -108,8 +175,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                               secretAccessKeySecretRef:
                                 description: The SecretAccessKey is used for authentication
@@ -130,8 +195,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                             type: object
                         type: object
@@ -179,8 +242,6 @@ spec:
                                   to. Ignored if referent is not cluster-scoped. cluster-scoped
                                   defaults to the namespace of the referent.
                                 type: string
-                            required:
-                            - name
                             type: object
                           clientSecret:
                             description: The Azure ClientSecret of the service principle
@@ -200,8 +261,6 @@ spec:
                                   to. Ignored if referent is not cluster-scoped. cluster-scoped
                                   defaults to the namespace of the referent.
                                 type: string
-                            required:
-                            - name
                             type: object
                         required:
                         - clientId
@@ -249,8 +308,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                             type: object
                         required:
@@ -259,6 +316,49 @@ spec:
                       projectID:
                         description: ProjectID project where secret is located
                         type: string
+                    type: object
+                  gitlab:
+                    description: GItlab configures this store to sync secrets using
+                      Gitlab Variables provider
+                    properties:
+                      auth:
+                        description: Auth configures how secret-manager authenticates
+                          with a GitLab instance.
+                        properties:
+                          SecretRef:
+                            properties:
+                              accessToken:
+                                description: AccessToken is used for authentication.
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                            type: object
+                        required:
+                        - SecretRef
+                        type: object
+                      projectID:
+                        description: ProjectID specifies a project where secrets are
+                          located.
+                        type: string
+                      url:
+                        description: URL configures the GitLab instance URL. Defaults
+                          to https://gitlab.com/.
+                        type: string
                     required:
                     - auth
                     type: object
@@ -291,8 +391,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                             type: object
                         required:
@@ -305,6 +403,76 @@ spec:
                     required:
                     - auth
                     type: object
+                  oracle:
+                    description: Oracle configures this store to sync secrets using
+                      Oracle Vault provider
+                    properties:
+                      auth:
+                        description: Auth configures how secret-manager authenticates
+                          with the Oracle Vault.
+                        properties:
+                          secretRef:
+                            description: SecretRef to pass through sensitive information.
+                            properties:
+                              fingerprint:
+                                description: projectID is an access token specific
+                                  to the secret.
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                              privatekey:
+                                description: The Access Token is used for authentication
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                            type: object
+                        required:
+                        - secretRef
+                        type: object
+                      region:
+                        description: projectID is an access token specific to the
+                          secret.
+                        type: string
+                      tenancy:
+                        description: projectID is an access token specific to the
+                          secret.
+                        type: string
+                      user:
+                        description: User is an access OCID specific to the account.
+                        type: string
+                    required:
+                    - auth
+                    type: object
                   vault:
                     description: Vault configures this store to sync secrets using
                       Hashi provider
@@ -351,8 +519,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                             required:
                             - path
@@ -384,8 +550,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                               secretRef:
                                 description: SecretRef to a key in a Secret resource
@@ -408,8 +572,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                             type: object
                           jwt:
@@ -441,8 +603,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                             type: object
                           kubernetes:
@@ -483,8 +643,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                               serviceAccountRef:
                                 description: Optional service account field containing
@@ -537,8 +695,6 @@ spec:
                                       cluster-scoped defaults to the namespace of
                                       the referent.
                                     type: string
-                                required:
-                                - name
                                 type: object
                               username:
                                 description: Username is a LDAP user name used to
@@ -566,8 +722,6 @@ spec:
                                   to. Ignored if referent is not cluster-scoped. cluster-scoped
                                   defaults to the namespace of the referent.
                                 type: string
-                            required:
-                            - name
                             type: object
                         type: object
                       caBundle:
@@ -608,6 +762,39 @@ spec:
                     - path
                     - server
                     type: object
+                  yandexlockbox:
+                    description: YandexLockbox configures this store to sync secrets
+                      using Yandex Lockbox provider
+                    properties:
+                      apiEndpoint:
+                        description: Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
+                        type: string
+                      auth:
+                        description: Auth defines the information necessary to authenticate
+                          against Yandex Lockbox
+                        properties:
+                          authorizedKeySecretRef:
+                            description: The authorized key used for authentication
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                        type: object
+                    required:
+                    - auth
+                    type: object
                 type: object
             required:
             - provider

+ 31 - 8
docs/contributing-devguide.md

@@ -8,6 +8,17 @@ git clone https://github.com/external-secrets/external-secrets.git
 cd external-secrets
 ```
 
+If you want to run controller tests you also need to install kubebuilder's `envtest`:
+
+```
+export KUBEBUILDER_TOOLS_VERSION='1.20.2' # check for latest version or a version that has support to what you are testing
+
+curl -sSLo envtest-bins.tar.gz "https://storage.googleapis.com/kubebuilder-tools/kubebuilder-tools-$KUBEBUILDER_TOOLS_VERSION-linux-amd64.tar.gz"
+
+sudo mkdir -p /usr/local/kubebuilder
+sudo tar -C /usr/local/kubebuilder --strip-components=1 -zvxf envtest-bins.tar.gz
+```
+
 ## Building & Testing
 
 The project uses the `make` build system. It'll run code generators, tests and
@@ -33,21 +44,19 @@ make docs
 
 ## Installing
 
-To install the External Secret Operator's CRDs into a Kubernetes Cluster run:
+To install the External Secret Operator into a Kubernetes Cluster run:
 
 ```shell
-make crds.install
+helm repo add external-secrets https://charts.external-secrets.io
+helm repo update
+helm install external-secrets external-secrets/external-secrets
 ```
 
-Apply the sample resources:
-```shell
-kubectl apply -f docs/snippets/basic-secret-store.yaml
-kubectl apply -f docs/snippets/basic-external-secret.yaml
-```
+You can alternatively run the controller on your host system for development purposes:
 
-You can run the controller on your host system for development purposes:
 
 ```shell
+make crds.install
 make run
 ```
 
@@ -57,6 +66,20 @@ To remove the CRDs run:
 make crds.uninstall
 ```
 
+If you need to test some other k8s integrations and need the operator to be deployed to the actuall cluster while developing, you can use the following workflow:
+
+```
+kind create cluster --name external-secrets
+
+export TAG=v2
+export IMAGE=eso-local
+
+docker build . -t $IMAGE:$TAG --build-arg TARGETARCH=amd64 --build-arg TARGETOS=linux
+
+make helm.generate
+helm upgrade --install external-secrets ./deploy/charts/external-secrets/ --set image.repository=$IMAGE --set image.tag=$TAG
+```
+
 !!! note "Contributing Flow"
     The HOW TO guide for contributing is at the [Contributing Process](contributing-process.md) page.
 

BIN
docs/pictures/screenshot_API_key.png


BIN
docs/pictures/screenshot_fingerprint.png


BIN
docs/pictures/screenshot_gitlab_projectID.png


BIN
docs/pictures/screenshot_gitlab_token.png


BIN
docs/pictures/screenshot_gitlab_token_created.png


BIN
docs/pictures/screenshot_region.png


BIN
docs/pictures/screenshot_tenancy_OCID.png


BIN
docs/pictures/screenshot_user_OCID.png


+ 1 - 1
docs/provider-aws-parameter-store.md

@@ -19,7 +19,7 @@ way users of the `SecretStore` can only access the secrets necessary.
 
 ### IAM Policy
 
-Create a IAM Policy to pin down access to secrets matching `dev-*`, for futher information see [AWS Documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html):
+Create a IAM Policy to pin down access to secrets matching `dev-*`, for further information see [AWS Documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html):
 
 ``` json
 {

+ 54 - 0
docs/provider-gitlab-project-variables.md

@@ -0,0 +1,54 @@
+## Gitlab Project Variables
+
+External Secrets Operator integrates with [Gitlab API](https://docs.gitlab.com/ee/api/project_level_variables.html) to sync Gitlab project variables to secrets held on the Kubernetes cluster.
+
+### Authentication
+
+The API requires an access token and project ID. To create a new access token, go to your user settings and select 'access tokens'. Give your token a name, expiration date, and select the permissions required (Note 'api' is required).
+
+![token-details](./pictures/screenshot_gitlab_token.png)
+
+Click 'Create personal access token', and your token will be generated and displayed on screen. Copy or save this token since you can't access it again.
+![token-created](./pictures/screenshot_gitlab_token_created.png)
+
+
+
+### Access Token secret
+
+Create a secret containing your access token:
+
+```yaml
+{% include 'gitlab-credentials-secret.yaml' %}
+```
+
+### Update secret store
+Be sure the `gitlab` provider is listed in the `Kind=SecretStore` and the ProjectID is set. If you are not using `https://gitlab.com`, you must set the `url` field as well.
+
+```yaml
+{% include 'gitlab-secret-store.yaml' %}
+```
+
+Your project ID can be found on your project's page.
+![projectID](./pictures/screenshot_gitlab_projectID.png)
+
+### Creating external secret
+
+To sync a Gitlab variable to a secret on the Kubernetes cluster, a `Kind=ExternalSecret` is needed.
+
+```yaml
+{% include 'gitlab-external-secret.yaml' %}
+```
+
+#### Using DataFrom
+
+DataFrom can be used to get a variable as a JSON string and attempt to parse it.
+
+```yaml
+{% include 'gitlab-external-secret-json.yaml' %}
+```
+
+### Getting the Kubernetes secret
+The operator will fetch the project variable and inject it as a `Kind=Secret`.
+```
+kubectl get secret gitlab-secret-to-create -o jsonpath='{.data.secretKey}' | base64 -d
+```

+ 61 - 5
docs/provider-google-secrets-manager.md

@@ -2,11 +2,7 @@
 
 External Secrets Operator integrates with [GCP Secret Manager](https://cloud.google.com/secret-manager) for secret management.
 
-### Authentication
-
-At the moment, we only support [service account key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) authentication.
-
-#### Service account key authentication
+### Service account key authentication
 
 A service account key is created and the JSON keyfile is stored in a `Kind=Secret`. The `project_id` and `private_key` should be configured for the project.
 
@@ -33,3 +29,63 @@ The operator will fetch the GCP Secret Manager secret and inject it as a `Kind=S
 ```
 kubectl get secret secret-to-be-created -n <namespace> | -o jsonpath='{.data.dev-secret-test}' | base64 -d
 ```
+
+## Authentication with Workload Identity
+
+This makes it possible for your Google Kubernetes Engine (GKE) applications to consume services provided by Google APIs, namely Secrets Manager service in this case.
+
+Here we will assume that you installed ESO using helm and that you named the chart installation `external-secrets` and the namespace where it lives `es` like:
+
+```sh
+helm install external-secrets external-secrets/external-secrets --namespace es
+```
+
+Then most of the resources would have this name, the important one here being the k8s service account attached to the external-secrets operator deployment:
+
+```
+# ...
+      containers:
+      - image: ghcr.io/external-secrets/external-secrets:vVERSION
+        name: external-secrets
+        ports:
+        - containerPort: 8080
+          protocol: TCP
+      restartPolicy: Always
+      schedulerName: default-scheduler
+      serviceAccount: external-secrets
+      serviceAccountName: external-secrets # <--- here
+```
+
+### Following the documentation
+
+You can find the documentation for Workload Identity under [this url](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity). We will walk you through how to navigate it here.
+
+#### Changing Values
+
+Search [the documment](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) for this editable values and change them to your values:
+
+- CLUSTER_NAME: The name of your cluster
+- PROJECT_ID: Your project ID (not your Project number nor your Project name)
+- K8S_NAMESPACE: For us folowing these steps here it will be `es`, but this will be the namespace where you deployed the external-secrets operator
+- KSA_NAME: external-secrets (if you are not creating a new one to attach to the deployemnt)
+- GSA_NAME: external-secrets for simplicity, or something else if you have to follow different naming convetions for cloud resources
+- ROLE_NAME: roles/secretmanager.secretAccessor so you make the pod only be able to access secrets on Secret Manager
+
+#### Following through
+
+You can follow through the documentation and adapt it to your specific use case. If you want to just use the serviceaccount that we deployed with the helm chart, for example, you don't need to create a new service account on 2 of [Authenticating to Google Cloud](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#authenticating_to).
+
+#### SecretStore with WorkloadIdentity
+
+To use workload identity you can just omit the auth field of the secret store and let the operator client fall back to defaults using the roles attached to your service account.
+
+```
+apiVersion: external-secrets.io/v1alpha1
+kind: SecretStore
+metadata:
+  name: example
+spec:
+  provider:
+    gcpsm:
+      projectID: pid
+```

+ 54 - 0
docs/provider-oracle-vault.md

@@ -0,0 +1,54 @@
+## Oracle Vault
+
+External Secrets Operator integrates with [OCI API](https://github.com/oracle/oci-go-sdk) to sync secret on the Oracle Vault to secrets held on the Kubernetes cluster.
+
+### Authentication
+
+The API requires a userOCID, tenancyOCID, fingerprint, key file and a region. The fingerprint and key file should be supplied in the secret with the rest being provided in the secret store.
+
+See url for what region you you are accessing.
+![userOCID-details](./pictures/screenshot_region.png)
+
+Select tenancy in the top right to see your user OCID as shown below.
+![tenancyOCID-details](./pictures/tenancy.png)
+
+Select your user in the top right to see your user OCID as shown below.
+![region-details](./pictures/screenshot_user_OCID.png)
+
+
+#### Service account key authentication
+
+Create a secret containing your private key and fingerprint:
+
+```yaml
+{% include 'oracle-credentials-secret.yaml' %}
+```
+
+Your fingerprint will be attatched to your API key, once it has been generated. Found on the same page as the user OCID.
+![fingerprint-details](./pictures/screenshot_fingerprint.png)
+
+Once you click "Add API Key" you will be shown the following, where you can download the RSA key in the necessary PEM format for API requests.
+This will automatically generate a fingerprint.
+![API-key-details](./pictures/screenshot_API_key.png)
+
+### Update secret store
+Be sure the `oracle` provider is listed in the `Kind=SecretStore`
+
+```yaml
+{% include 'oracle-secret-store.yaml' %}
+```
+
+### Creating external secret
+
+To create a kubernetes secret from the Oracle Cloud Interface secret a`Kind=ExternalSecret` is needed.
+
+```yaml
+{% include 'oracle-external-secret.yaml' %}
+```
+
+
+### Getting the Kubernetes secret
+The operator will fetch the project variable and inject it as a `Kind=Secret`.
+```
+kubectl get secret oracle-secret-to-create -o jsonpath='{.data.dev-secret-test}' | base64 -d
+```

+ 86 - 0
docs/provider-yandex-lockbox.md

@@ -0,0 +1,86 @@
+## Yandex Lockbox
+
+External Secrets Operator integrates with [Yandex Lockbox](https://cloud.yandex.com/docs/lockbox/)
+for secret management.
+
+### Prerequisites
+* [External Secrets Operator installed](../guides-getting-started/#installing-with-helm)
+* [Yandex.Cloud CLI installed](https://cloud.yandex.com/docs/cli/quickstart)
+
+### Authentication
+At the moment, [authorized key](https://cloud.yandex.com/docs/iam/concepts/authorization/key) authentication is only supported:
+
+* Create a [service account](https://cloud.yandex.com/docs/iam/concepts/users/service-accounts) in Yandex.Cloud:
+```bash
+yc iam service-account create --name eso-service-account
+```
+* Create an authorized key for the service account and save it to `authorized-key.json` file:
+```bash
+yc iam key create \
+  --service-account-name eso-service-account \
+  --output authorized-key.json
+```
+* Create a k8s secret containing the authorized key saved above:
+```bash
+kubectl create secret generic yc-auth --from-file=authorized-key=authorized-key.json
+```
+* Create a [SecretStore](../api-secretstore/) pointing to `yc-auth` k8s secret:
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: SecretStore
+metadata:
+  name: secret-store
+spec:
+  provider:
+    yandexlockbox:
+      auth:
+        authorizedKeySecretRef:
+          name: yc-auth
+          key: authorized-key
+```
+
+### Creating external secret
+To make External Secrets Operator sync a k8s secret with a Lockbox secret:
+
+* Create a Lockbox secret, if not already created:
+```bash
+yc lockbox secret create \
+  --name lockbox-secret \
+  --payload '[{"key": "password","textValue": "p@$$w0rd"}]'
+```
+* Assign the [`lockbox.payloadViewer`](https://cloud.yandex.com/docs/lockbox/security/#roles-list) role
+  for accessing the `lockbox-secret` payload to the service account used for authentication:
+```bash
+yc lockbox secret add-access-binding \
+  --name lockbox-secret \
+  --service-account-name eso-service-account \
+  --role lockbox.payloadViewer
+```
+Run the following command to ensure that the correct access binding has been added:
+```bash
+yc lockbox secret list-access-bindings --name lockbox-secret
+```
+* Create an [ExternalSecret](../api-externalsecret/) pointing to `secret-store` and `lockbox-secret`:
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: external-secret
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: secret-store
+    kind: SecretStore
+  target:
+    name: k8s-secret # the target k8s secret name
+  data:
+  - secretKey: password # the target k8s secret key
+    remoteRef:
+      key: ***** # ID of lockbox-secret
+      property: password # (optional) payload entry key of lockbox-secret
+```
+
+The operator will fetch the Yandex Lockbox secret and inject it as a `Kind=Secret`
+```yaml
+kubectl get secret k8s-secret -n <namespace> | -o jsonpath='{.data.password}' | base64 -d
+```

+ 9 - 0
docs/snippets/gitlab-credentials-secret.yaml

@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: gitlab-secret
+  labels: 
+    type: gitlab
+type: Opaque 
+stringData:
+  token: "**access token goes here**"

+ 18 - 0
docs/snippets/gitlab-external-secret-json.yaml

@@ -0,0 +1,18 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: gitlab-external-secret-example
+spec:
+  refreshInterval: 1h
+
+  secretStoreRef:
+    kind: SecretStore
+    name: gitlab-secret-store # Must match SecretStore on the cluster
+
+  target:
+    name: gitlab-secret-to-create # Name for the secret to be created on the cluster
+    creationPolicy: Owner
+
+  # each secret name in the KV will be used as the secret key in the SECRET k8s target object
+  dataFrom:
+  - key: "myJsonVariable" # Key of the variable on Gitlab

+ 19 - 0
docs/snippets/gitlab-external-secret.yaml

@@ -0,0 +1,19 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: gitlab-external-secret-example
+spec:
+  refreshInterval: 1h
+
+  secretStoreRef:
+    kind: SecretStore
+    name: gitlab-secret-store # Must match SecretStore on the cluster
+
+  target:
+    name: gitlab-secret-to-create # Name for the secret to be created on the cluster
+    creationPolicy: Owner
+
+  data:
+    - secretKey: secretKey # Key given to the secret to be created on the cluster
+      remoteRef: 
+        key: myGitlabVariable # Key of the variable on Gitlab

+ 15 - 0
docs/snippets/gitlab-secret-store.yaml

@@ -0,0 +1,15 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: SecretStore
+metadata:
+  name: gitlab-secret-store
+spec:
+  provider:
+    # provider type: gitlab
+    gitlab:
+      # url: https://gitlab.mydomain.com/
+      auth:
+        SecretRef:
+          accessToken:
+            name: gitlab-secret
+            key: token
+      projectID: "**project ID goes here**"

+ 10 - 0
docs/snippets/oracle-credentials-secret.yaml

@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: oracle-secret
+  labels: 
+    type: oracle
+type: Opaque
+stringData:
+  privateKey: 
+  fingerprint: 

+ 16 - 0
docs/snippets/oracle-external-secret.yaml

@@ -0,0 +1,16 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: example
+spec:
+  refreshInterval: 0.03m
+  secretStoreRef:
+    kind: SecretStore
+    name: example # Must match SecretStore on the cluster
+  target:
+    name: secret-to-be-created # Name for the secret on the cluster
+    creationPolicy: Owner
+  data:
+  - secretKey: 
+    remoteRef:
+      key: 

+ 18 - 0
docs/snippets/oracle-secret-store.yaml

@@ -0,0 +1,18 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: SecretStore
+metadata:
+  name: example
+spec:
+  provider:
+    oracle: #Needs to match value in secretstore_types.go
+      user: 
+      tenancy: 
+      region: 
+      auth:
+        secretRef:
+          privatekey:
+            name: oracle-secret
+            key: privateKey #Needs to match stringData val in secret_oracle.yml
+          fingerprint:
+            name: oracle-secret
+            key: fingerprint

+ 3 - 0
docs/snippets/provider-aws-access.md

@@ -19,6 +19,7 @@ spec:
   provider:
     aws:
       service: SecretsManager
+      region: eu-central-1
       # optional: do a sts:assumeRole before fetching secrets
       role: team-b
 ```
@@ -37,6 +38,7 @@ spec:
   provider:
     aws:
       service: SecretsManager
+      region: eu-central-1
       # optional: assume role before fetching secrets
       role: team-b
       auth:
@@ -78,6 +80,7 @@ spec:
   provider:
     aws:
       service: SecretsManager
+      region: eu-central-1
       auth:
         jwt:
           serviceAccountRef:

+ 2 - 1
docs/snippets/vault-approle-store.yaml

@@ -18,7 +18,8 @@ spec:
           path: "approle"
           # RoleID configured in the App Role authentication backend
           roleId: "db02de05-fa39-4855-059b-67221c5c2f63"
+          # Reference to a key in a K8 Secret that contains the App Role SecretId
           secretRef:
             name: "my-secret"
             namespace: "secret-admin"
-            key: "vault-token"
+            key: "secret-id"

+ 219 - 1
docs/spec.md

@@ -543,7 +543,9 @@ ExternalSecretStatus
 <th>Description</th>
 </tr>
 </thead>
-<tbody><tr><td><p>&#34;Ready&#34;</p></td>
+<tbody><tr><td><p>&#34;Deleted&#34;</p></td>
+<td></td>
+</tr><tr><td><p>&#34;Ready&#34;</p></td>
 <td></td>
 </tr></tbody>
 </table>
@@ -1151,6 +1153,7 @@ GCPSMAuth
 </em>
 </td>
 <td>
+<em>(Optional)</em>
 <p>Auth defines the information necessary to authenticate against GCP</p>
 </td>
 </tr>
@@ -1173,6 +1176,119 @@ string
 <p>GenericStore is a common interface for interacting with ClusterSecretStore
 or a namespaced SecretStore.</p>
 </p>
+<h3 id="external-secrets.io/v1alpha1.GitlabAuth">GitlabAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.GitlabProvider">GitlabProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>SecretRef</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.GitlabSecretRef">
+GitlabSecretRef
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.GitlabProvider">GitlabProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>Configures a store to sync secrets with a GitLab instance.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>url</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>URL configures the GitLab instance URL. Defaults to <a href="https://gitlab.com/">https://gitlab.com/</a>.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.GitlabAuth">
+GitlabAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth configures how secret-manager authenticates with a GitLab instance.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>projectID</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>ProjectID specifies a project where secrets are located.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.GitlabSecretRef">GitlabSecretRef
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.GitlabAuth">GitlabAuth</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>accessToken</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+<p>AccessToken is used for authentication.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1alpha1.IBMAuth">IBMAuth
 </h3>
 <p>
@@ -1466,6 +1582,34 @@ IBMProvider
 <p>IBM configures this store to sync secrets using IBM Cloud provider</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>yandexlockbox</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.YandexLockboxProvider">
+YandexLockboxProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>YandexLockbox configures this store to sync secrets using Yandex Lockbox provider</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>gitlab</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.GitlabProvider">
+GitlabProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>GItlab configures this store to sync secrets using Gitlab Variables provider</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1alpha1.SecretStoreRef">SecretStoreRef
@@ -2274,6 +2418,80 @@ are used to validate the TLS connection.</p>
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1alpha1.YandexLockboxAuth">YandexLockboxAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.YandexLockboxProvider">YandexLockboxProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>authorizedKeySecretRef</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>The authorized key used for authentication</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.YandexLockboxProvider">YandexLockboxProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>YandexLockboxProvider Configures a store to sync secrets using the Yandex Lockbox provider.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>apiEndpoint</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Yandex.Cloud API endpoint (e.g. &lsquo;api.cloud.yandex.net:443&rsquo;)</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.YandexLockboxAuth">
+YandexLockboxAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth defines the information necessary to authenticate against Yandex Lockbox</p>
+</td>
+</tr>
+</tbody>
+</table>
 <hr/>
 <p><em>
 Generated with <code>gen-crd-api-reference-docs</code>.

BIN
e2e/.DS_Store


+ 3 - 0
e2e/e2e_test.go

@@ -40,6 +40,9 @@ var _ = SynchronizedBeforeSuite(func() []byte {
 
 	By("installing eso")
 	addon.InstallGlobalAddon(addon.NewESO(), cfg)
+
+	By("installing scoped eso")
+	addon.InstallGlobalAddon(addon.NewScopedESO(), cfg)
 	return nil
 }, func([]byte) {})
 

+ 11 - 0
e2e/framework/addon/eso.go

@@ -27,3 +27,14 @@ func NewESO() *ESO {
 		},
 	}
 }
+
+func NewScopedESO() *ESO {
+	return &ESO{
+		&HelmChart{
+			Namespace:   "default",
+			ReleaseName: "eso-aws-sm",
+			Chart:       "/k8s/deploy/charts/external-secrets",
+			Values:      []string{"/k8s/eso.scoped.values.yaml"},
+		},
+	}
+}

+ 9 - 0
e2e/framework/eso.go

@@ -11,6 +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 framework
 
 import (
@@ -23,6 +24,8 @@ import (
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/wait"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 )
 
 // WaitForSecretValue waits until a secret comes into existence and compares the secret.Data
@@ -52,6 +55,12 @@ func equalSecrets(exp, ts *v1.Secret) bool {
 		return false
 	}
 
+	// secret contains data hash property which must be ignored
+	delete(ts.ObjectMeta.Annotations, esv1alpha1.AnnotationDataHash)
+	if len(ts.ObjectMeta.Annotations) == 0 {
+		ts.ObjectMeta.Annotations = nil
+	}
+
 	expAnnotations, _ := json.Marshal(exp.ObjectMeta.Annotations)
 	tsAnnotations, _ := json.Marshal(ts.ObjectMeta.Annotations)
 	if !bytes.Equal(expAnnotations, tsAnnotations) {

+ 12 - 0
e2e/k8s/eso.scoped.values.yaml

@@ -0,0 +1,12 @@
+installCRDs: false
+image:
+  repository: local/external-secrets
+  tag: test
+scopedNamespace: test
+extraEnv:
+  - name: AWS_SECRETSMANAGER_ENDPOINT
+    value: "http://localstack.default"
+  - name: AWS_STS_ENDPOINT
+    value: "http://localstack.default"
+  - name: AWS_SSM_ENDPOINT
+    value: "http://localstack.default"

+ 7 - 0
e2e/run.sh

@@ -58,5 +58,12 @@ kubectl run --rm \
   --env="AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET:-}" \
   --env="TENANT_ID=${TENANT_ID:-}" \
   --env="VAULT_URL=${VAULT_URL:-}" \
+  --env="GITLAB_TOKEN=${GITLAB_TOKEN:-}" \
+  --env="GITLAB_PROJECT_ID=${GITLAB_PROJECT_ID:-}" \
+  --env="ORACLE_USER_OCID=${ORACLE_USER_OCID:-}" \
+  --env="ORACLE_TENANCY_OCID=${ORACLE_TENANCY_OCID:-}" \
+  --env="ORACLE_REGION=${ORACLE_REGION:-}" \
+  --env="ORACLE_FINGERPRINT=${ORACLE_FINGERPRINT:-}" \
+  --env="ORACLE_KEY=${ORACLE_KEY:-}" \
   --overrides='{ "apiVersion": "v1", "spec":{"serviceAccountName": "external-secrets-e2e"}}' \
   e2e --image=local/external-secrets-e2e:test

+ 47 - 0
e2e/suite/alibaba/alibaba.go

@@ -0,0 +1,47 @@
+/*
+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 alibaba
+
+import (
+	"os"
+
+	// nolint
+	. "github.com/onsi/ginkgo"
+	// nolint
+	. "github.com/onsi/ginkgo/extensions/table"
+
+	"github.com/external-secrets/external-secrets/e2e/framework"
+	"github.com/external-secrets/external-secrets/e2e/suite/common"
+)
+
+var _ = Describe("[alibaba] ", func() {
+	f := framework.New("eso-alibaba")
+	accessKeyID := os.Getenv("ACCESS_KEY_ID")
+	accessKeySecret := os.Getenv("ACCESS_KEY_SECRET")
+	regionID := os.Getenv("REGION_ID")
+	prov := newAlibabaProvider(f, accessKeyID, accessKeySecret, regionID)
+
+	DescribeTable("sync secrets", framework.TableFunc(f, prov),
+		Entry(common.SimpleDataSync(f)),
+		Entry(common.NestedJSONWithGJSON(f)),
+		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataWithProperty(f)),
+		Entry(common.JSONDataWithTemplate(f)),
+		Entry(common.DockerJSONConfig(f)),
+		Entry(common.DataPropertyDockerconfigJSON(f)),
+		Entry(common.SSHKeySync(f)),
+		Entry(common.SSHKeySyncDataProperty(f)),
+	)
+})

+ 118 - 0
e2e/suite/alibaba/provider.go

@@ -0,0 +1,118 @@
+/*
+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 alibaba
+
+import (
+	"context"
+
+	"github.com/aliyun/alibaba-cloud-sdk-go/services/kms"
+
+	//nolint
+	. "github.com/onsi/ginkgo"
+
+	//nolint
+	. "github.com/onsi/gomega"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/e2e/framework"
+)
+
+type alibabaProvider struct {
+	accessKeyID     string
+	accessKeySecret string
+	regionID        string
+	framework       *framework.Framework
+}
+
+const (
+	secretName = "secretName"
+)
+
+func newAlibabaProvider(f *framework.Framework, accessKeyID, accessKeySecret, regionID string) *alibabaProvider {
+	prov := &alibabaProvider{
+		accessKeyID:     accessKeyID,
+		accessKeySecret: accessKeySecret,
+		regionID:        regionID,
+		framework:       f,
+	}
+	BeforeEach(prov.BeforeEach)
+	return prov
+}
+
+// CreateSecret creates a secret in both kv v1 and v2 provider.
+func (s *alibabaProvider) CreateSecret(key, val string) {
+	client, err := kms.NewClientWithAccessKey(s.regionID, s.accessKeyID, s.accessKeySecret)
+	Expect(err).ToNot(HaveOccurred())
+	kmssecretrequest := kms.CreateCreateSecretRequest()
+	kmssecretrequest.SecretName = secretName
+	kmssecretrequest.SecretData = "value"
+	_, err = client.CreateSecret(kmssecretrequest)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (s *alibabaProvider) DeleteSecret(key string) {
+	client, err := kms.NewClientWithAccessKey(s.regionID, s.accessKeyID, s.accessKeySecret)
+	Expect(err).ToNot(HaveOccurred())
+	kmssecretrequest := kms.CreateDeleteSecretRequest()
+	kmssecretrequest.SecretName = secretName
+	_, err = client.DeleteSecret(kmssecretrequest)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (s *alibabaProvider) BeforeEach() {
+	// Creating an Alibaba secret
+	alibabaCreds := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      secretName,
+			Namespace: s.framework.Namespace.Name,
+		},
+		StringData: map[string]string{
+			secretName: "value",
+		},
+	}
+	err := s.framework.CRClient.Create(context.Background(), alibabaCreds)
+	Expect(err).ToNot(HaveOccurred())
+
+	// Creating Alibaba secret store
+	secretStore := &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      s.framework.Namespace.Name,
+			Namespace: s.framework.Namespace.Name,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				Alibaba: &esv1alpha1.AlibabaProvider{
+					Auth: &esv1alpha1.AlibabaAuth{
+						SecretRef: esv1alpha1.AlibabaAuthSecretRef{
+							AccessKeyID: esmeta.SecretKeySelector{
+								Name: "kms-secret",
+								Key:  "keyid",
+							},
+							AccessKeySecret: esmeta.SecretKeySelector{
+								Name: "kms-secret",
+								Key:  "accesskey",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	err = s.framework.CRClient.Create(context.Background(), secretStore)
+	Expect(err).ToNot(HaveOccurred())
+}

+ 1 - 0
e2e/suite/aws/provider.go

@@ -11,6 +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 aws
 
 import (

+ 1 - 0
e2e/suite/aws/secretsmanager.go

@@ -11,6 +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 aws
 
 import (

+ 45 - 0
e2e/suite/gitlab/gitlab.go

@@ -0,0 +1,45 @@
+/*
+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.
+limitations under the License.
+*/
+package gitlab
+
+// TODO - Gitlab only accepts variable names with alphanumeric and '_'
+// whereas ESO only accepts names with alphanumeric and '-'.
+// Current workaround is to remove all hyphens and underscores set in e2e/framework/util/util.go
+// and in e2e/suite/common/common.go, but this breaks Azure provider.
+
+import (
+	"os"
+
+	// nolint
+	. "github.com/onsi/ginkgo"
+	// nolint
+	. "github.com/onsi/ginkgo/extensions/table"
+
+	"github.com/external-secrets/external-secrets/e2e/framework"
+	"github.com/external-secrets/external-secrets/e2e/suite/common"
+)
+
+var _ = Describe("[gitlab] ", func() {
+	f := framework.New("esogitlab")
+	credentials := os.Getenv("GITLAB_TOKEN")
+	projectID := os.Getenv("GITLAB_PROJECT_ID")
+	prov := newGitlabProvider(f, credentials, projectID)
+
+	DescribeTable("sync secrets", framework.TableFunc(f, prov),
+		Entry(common.SimpleDataSync(f)),
+		Entry(common.JSONDataWithProperty(f)),
+		Entry(common.JSONDataFromSync(f)),
+		Entry(common.NestedJSONWithGJSON(f)),
+		Entry(common.JSONDataWithTemplate(f)),
+	)
+})

+ 131 - 0
e2e/suite/gitlab/provider.go

@@ -0,0 +1,131 @@
+/*
+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 gitlab
+
+import (
+	"context"
+	"strings"
+
+	// nolint
+	. "github.com/onsi/ginkgo"
+
+	// nolint
+	. "github.com/onsi/gomega"
+	gitlab "github.com/xanzy/go-gitlab"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/e2e/framework"
+)
+
+type gitlabProvider struct {
+	credentials string
+	projectID   string
+	framework   *framework.Framework
+}
+
+func newGitlabProvider(f *framework.Framework, credentials, projectID string) *gitlabProvider {
+	prov := &gitlabProvider{
+		credentials: credentials,
+		projectID:   projectID,
+		framework:   f,
+	}
+	BeforeEach(prov.BeforeEach)
+	return prov
+}
+
+func (s *gitlabProvider) CreateSecret(key, val string) {
+	// **Open the client
+	client, err := gitlab.NewClient(s.credentials)
+	Expect(err).ToNot(HaveOccurred())
+	// Open the client**
+
+	// Set variable options
+	variableKey := strings.ReplaceAll(key, "-", "_")
+	variableValue := val
+
+	opt := gitlab.CreateProjectVariableOptions{
+		Key:              &variableKey,
+		Value:            &variableValue,
+		VariableType:     nil,
+		Protected:        nil,
+		Masked:           nil,
+		EnvironmentScope: nil,
+	}
+
+	// Create a variable
+	_, _, err = client.ProjectVariables.CreateVariable(s.projectID, &opt)
+
+	Expect(err).ToNot(HaveOccurred())
+	// Versions aren't supported by Gitlab, but we could add
+	// more parameters to test
+}
+
+func (s *gitlabProvider) DeleteSecret(key string) {
+	// **Open a client
+	client, err := gitlab.NewClient(s.credentials)
+	Expect(err).ToNot(HaveOccurred())
+	// Open a client**
+
+	// Delete the secret
+	_, err = client.ProjectVariables.RemoveVariable(s.projectID, strings.ReplaceAll(key, "-", "_"))
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (s *gitlabProvider) BeforeEach() {
+	By("creating a gitlab variable")
+	gitlabCreds := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "provider-secret",
+			Namespace: s.framework.Namespace.Name,
+		},
+		// Puts access token into StringData
+
+		StringData: map[string]string{
+			"token":     s.credentials,
+			"projectID": s.projectID,
+		},
+	}
+	err := s.framework.CRClient.Create(context.Background(), gitlabCreds)
+	Expect(err).ToNot(HaveOccurred())
+
+	// Create a secret store - change these values to match YAML
+	By("creating a secret store for credentials")
+	secretStore := &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      s.framework.Namespace.Name,
+			Namespace: s.framework.Namespace.Name,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				Gitlab: &esv1alpha1.GitlabProvider{
+					ProjectID: s.projectID,
+					Auth: esv1alpha1.GitlabAuth{
+						SecretRef: esv1alpha1.GitlabSecretRef{
+							AccessToken: esmeta.SecretKeySelector{
+								Name: "provider-secret",
+								Key:  "token",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	err = s.framework.CRClient.Create(context.Background(), secretStore)
+	Expect(err).ToNot(HaveOccurred())
+}

+ 47 - 0
e2e/suite/oracle/oracle.go

@@ -0,0 +1,47 @@
+/*
+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.
+limitations under the License.
+*/
+package oracle
+
+import (
+	"os"
+
+	// nolint
+	. "github.com/onsi/ginkgo"
+	// nolint
+	. "github.com/onsi/ginkgo/extensions/table"
+
+	"github.com/external-secrets/external-secrets/e2e/framework"
+	"github.com/external-secrets/external-secrets/e2e/suite/common"
+)
+
+var _ = Describe("[oracle] ", func() {
+	f := framework.New("eso-oracle")
+	tenancy := os.Getenv("OCI_TENANCY_OCID")
+	user := os.Getenv("OCI_USER_OCID")
+	region := os.Getenv("OCI_REGION")
+	fingerprint := os.Getenv("OCI_FINGERPRINT")
+	privateKey := os.Getenv("OCI_PRIVATE_KEY")
+	prov := newOracleProvider(f, tenancy, user, region, fingerprint, privateKey)
+
+	DescribeTable("sync secrets", framework.TableFunc(f, prov),
+		Entry(common.SimpleDataSync(f)),
+		Entry(common.NestedJSONWithGJSON(f)),
+		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataWithProperty(f)),
+		Entry(common.JSONDataWithTemplate(f)),
+		Entry(common.DockerJSONConfig(f)),
+		Entry(common.DataPropertyDockerconfigJSON(f)),
+		Entry(common.SSHKeySync(f)),
+		Entry(common.SSHKeySyncDataProperty(f)),
+	)
+})

+ 124 - 0
e2e/suite/oracle/provider.go

@@ -0,0 +1,124 @@
+/*
+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.
+limitations under the License.
+*/
+package oracle
+
+import (
+	"context"
+
+	// nolint
+	. "github.com/onsi/ginkgo"
+
+	// nolint
+	. "github.com/onsi/gomega"
+	"github.com/oracle/oci-go-sdk/v45/common"
+	vault "github.com/oracle/oci-go-sdk/v45/vault"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	utilpointer "k8s.io/utils/pointer"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/e2e/framework"
+)
+
+type oracleProvider struct {
+	tenancy     string
+	user        string
+	region      string
+	fingerprint string
+	privateKey  string
+	framework   *framework.Framework
+	ctx         context.Context
+}
+
+const (
+	secretName = "secretName"
+)
+
+func newOracleProvider(f *framework.Framework, tenancy, user, region, fingerprint, privateKey string) *oracleProvider {
+	prov := &oracleProvider{
+		tenancy:     tenancy,
+		user:        user,
+		region:      region,
+		fingerprint: fingerprint,
+		privateKey:  privateKey,
+		framework:   f,
+	}
+	BeforeEach(prov.BeforeEach)
+	return prov
+}
+
+func (p *oracleProvider) CreateSecret(key, val string) {
+	configurationProvider := common.NewRawConfigurationProvider(p.tenancy, p.user, p.region, p.fingerprint, p.privateKey, nil)
+	client, err := vault.NewVaultsClientWithConfigurationProvider(configurationProvider)
+	Expect(err).ToNot(HaveOccurred())
+	vmssecretrequest := vault.CreateSecretRequest{}
+	vmssecretrequest.SecretName = utilpointer.StringPtr(secretName)
+	vmssecretrequest.SecretContent = vault.Base64SecretContentDetails{
+		Name:    utilpointer.StringPtr(key),
+		Content: utilpointer.StringPtr(val),
+	}
+	_, err = client.CreateSecret(p.ctx, vmssecretrequest)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (p *oracleProvider) DeleteSecret(key string) {
+	configurationProvider := common.NewRawConfigurationProvider(p.tenancy, p.user, p.region, p.fingerprint, p.privateKey, nil)
+	client, err := vault.NewVaultsClientWithConfigurationProvider(configurationProvider)
+	Expect(err).ToNot(HaveOccurred())
+	vmssecretrequest := vault.ScheduleSecretDeletionRequest{}
+	vmssecretrequest.SecretId = utilpointer.StringPtr(key)
+	_, err = client.ScheduleSecretDeletion(p.ctx, vmssecretrequest)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (p *oracleProvider) BeforeEach() {
+	OracleCreds := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      secretName,
+			Namespace: p.framework.Namespace.Name,
+		},
+		StringData: map[string]string{
+			secretName: "value",
+		},
+	}
+	err := p.framework.CRClient.Create(context.Background(), OracleCreds)
+	Expect(err).ToNot(HaveOccurred())
+
+	secretStore := &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      p.framework.Namespace.Name,
+			Namespace: p.framework.Namespace.Name,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				Oracle: &esv1alpha1.OracleProvider{
+					Auth: esv1alpha1.OracleAuth{
+						SecretRef: esv1alpha1.OracleSecretRef{
+							Fingerprint: esmeta.SecretKeySelector{
+								Name: "vms-secret",
+								Key:  "keyid",
+							},
+							PrivateKey: esmeta.SecretKeySelector{
+								Name: "vms-secret",
+								Key:  "accesskey",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	err = p.framework.CRClient.Create(context.Background(), secretStore)
+	Expect(err).ToNot(HaveOccurred())
+}

+ 12 - 6
go.mod

@@ -3,8 +3,9 @@ module github.com/external-secrets/external-secrets
 go 1.16
 
 replace (
+	github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1 => ./apis/externalsecrets/v1alpha1
+	github.com/external-secrets/external-secrets/pkg/provider/gitlab => ./pkg/provider/gitlab
 	google.golang.org/grpc => google.golang.org/grpc v1.27.0
-
 	k8s.io/api => k8s.io/api v0.21.2
 	k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.21.2
 	k8s.io/apimachinery => k8s.io/apimachinery v0.21.2
@@ -39,6 +40,7 @@ require (
 	github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
 	github.com/IBM/go-sdk-core/v5 v5.5.0
 	github.com/IBM/secrets-manager-go-sdk v1.0.23
+	github.com/aliyun/alibaba-cloud-sdk-go v1.61.1192
 	github.com/aws/aws-sdk-go v1.38.6
 	github.com/crossplane/crossplane-runtime v0.13.0
 	github.com/fatih/color v1.10.0 // indirect
@@ -51,19 +53,22 @@ require (
 	github.com/googleapis/gax-go v1.0.3
 	github.com/hashicorp/go-hclog v0.14.1 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
-	github.com/hashicorp/go-retryablehttp v0.6.7 // indirect
 	github.com/hashicorp/hcl v1.0.1-vault // indirect
 	github.com/hashicorp/vault/api v1.0.5-0.20210224012239-b540be4b7ec4
 	github.com/kr/pretty v0.2.1 // indirect
 	github.com/lestrrat-go/jwx v1.2.1
 	github.com/onsi/ginkgo v1.16.4
-	github.com/onsi/gomega v1.13.0
+	github.com/onsi/gomega v1.16.0
+	github.com/oracle/oci-go-sdk/v45 v45.2.0
 	github.com/pierrec/lz4 v2.5.2+incompatible // indirect
 	github.com/prometheus/client_golang v1.11.0
 	github.com/prometheus/client_model v0.2.0
 	github.com/spf13/cobra v1.1.3 // indirect
 	github.com/stretchr/testify v1.7.0
 	github.com/tidwall/gjson v1.7.5
+	github.com/xanzy/go-gitlab v0.50.1
+	github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588
+	github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa
 	github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
 	go.uber.org/zap v1.17.0
 	golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
@@ -72,12 +77,13 @@ require (
 	golang.org/x/tools v0.1.2-0.20210512205948-8287d5da45e4 // indirect
 	google.golang.org/api v0.30.0
 	google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a
+	google.golang.org/grpc v1.31.0
 	honnef.co/go/tools v0.1.4 // indirect
-	k8s.io/api v0.21.2
-	k8s.io/apimachinery v0.21.2
+	k8s.io/api v0.21.3
+	k8s.io/apimachinery v0.21.3
 	k8s.io/client-go v0.21.2
 	k8s.io/utils v0.0.0-20210527160623-6fdb442a123b
-	sigs.k8s.io/controller-runtime v0.9.2
+	sigs.k8s.io/controller-runtime v0.9.3
 	sigs.k8s.io/controller-tools v0.5.0
 	software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
 )

+ 35 - 5
go.sum

@@ -77,6 +77,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
+github.com/aliyun/alibaba-cloud-sdk-go v1.61.1192 h1:rRuMCkcoxoQ/kWSBN190JmD292PrYnpl7KyRWhYrjnY=
+github.com/aliyun/alibaba-cloud-sdk-go v1.61.1192/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@@ -98,6 +100,7 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
@@ -135,6 +138,7 @@ github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D
 github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
 github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 h1:sgNeV1VRMDzs6rzyPpxyM0jp317hnwiq58Filgag2xw=
 github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0/go.mod h1:J70FGZSbzsjecRTiTzER+3f1KZLNaXkuv+yeFTKoxM8=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
@@ -166,6 +170,7 @@ github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -242,6 +247,7 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
@@ -291,6 +297,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
@@ -320,6 +328,7 @@ github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3i
 github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
 github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=
 github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@@ -352,8 +361,8 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9
 github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
 github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
 github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
-github.com/hashicorp/go-retryablehttp v0.6.7 h1:8/CAEZt/+F7kR7GevNHulKkUjLht3CPmn7egmhieNKo=
-github.com/hashicorp/go-retryablehttp v0.6.7/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
+github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs=
+github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
 github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
@@ -401,6 +410,7 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
 github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -408,6 +418,7 @@ github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMW
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
@@ -479,6 +490,7 @@ github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
 github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
 github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
@@ -530,8 +542,11 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
-github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
 github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
+github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
+github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/oracle/oci-go-sdk/v45 v45.2.0 h1:vCPoQlE+DOrM2heJn66rvPU6fbsc/0Cxtzs2jnFut6U=
+github.com/oracle/oci-go-sdk/v45 v45.2.0/go.mod h1:ZM6LGiRO5TPQJxTlrXbcHMbClE775wnGD5U/EerCsRw=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -594,7 +609,10 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@@ -640,11 +658,17 @@ github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqri
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/xanzy/go-gitlab v0.50.1 h1:eH1G0/ZV1j81rhGrtbcePjbM5Ern7mPA4Xjt+yE+2PQ=
+github.com/xanzy/go-gitlab v0.50.1/go.mod h1:Q+hQhV508bDPoBijv7YjK/Lvlb4PhVhJdKqXVQrUoAE=
 github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
 github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
 github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588 h1:Lbz8X5Nre0Lg5QgCblmo0AhScWxeN3CVnX+mZ5Hxksk=
+github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
+github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa h1:Un1jWl/YWbK1179aMbsEZ6uLlDjjBAjL8KXldho1Umo=
+github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa/go.mod h1:UkgAKjyQo+Pylt2HTYz/G0PgnxmKOJ9IX/3XiRYQ9Ns=
 github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
 github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
 github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
@@ -763,6 +787,7 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
@@ -781,6 +806,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -969,6 +995,7 @@ google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
 google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
 google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w=
 google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
@@ -996,6 +1023,7 @@ google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfG
 google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200323114720-3f67cca34472/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
@@ -1039,6 +1067,8 @@ gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+a
 gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
 gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
@@ -1104,8 +1134,8 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
 sigs.k8s.io/controller-runtime v0.8.0/go.mod h1:v9Lbj5oX443uR7GXYY46E0EE2o7k2YxQ58GxVNeXSW4=
-sigs.k8s.io/controller-runtime v0.9.2 h1:MnCAsopQno6+hI9SgJHKddzXpmv2wtouZz6931Eax+Q=
-sigs.k8s.io/controller-runtime v0.9.2/go.mod h1:TxzMCHyEUpaeuOiZx/bIdc2T81vfs/aKdvJt9wuu0zk=
+sigs.k8s.io/controller-runtime v0.9.3 h1:n075bHQ1wb8hpX7C27pNrqsb0fj8mcfCQfNX+oKTbYE=
+sigs.k8s.io/controller-runtime v0.9.3/go.mod h1:TxzMCHyEUpaeuOiZx/bIdc2T81vfs/aKdvJt9wuu0zk=
 sigs.k8s.io/controller-tools v0.2.4/go.mod h1:m/ztfQNocGYBgTTCmFdnK94uVvgxeZeE3LtJvd/jIzA=
 sigs.k8s.io/controller-tools v0.5.0 h1:3u2RCwOlp0cjCALAigpOcbAf50pE+kHSdueUosrC/AE=
 sigs.k8s.io/controller-tools v0.5.0/go.mod h1:JTsstrMpxs+9BUj6eGuAaEb6SDSPTeVtUyp0jmnAM/I=

+ 6 - 0
hack/api-docs/mkdocs.yml

@@ -46,6 +46,12 @@ nav:
     - IBM:
       - Secrets Manager: provider-ibm-secrets-manager.md
     - HashiCorp Vault: provider-hashicorp-vault.md
+    - Yandex:
+        - Lockbox: provider-yandex-lockbox.md
+    - Gitlab:
+      - Gitlab Project Variables: provider-gitlab-project-variables.md
+    - Oracle:
+      - Oracle Vault: provider-oracle-vault.md
   - References:
     - API specification: spec.md
   - Contributing:

+ 4 - 1
main.go

@@ -46,18 +46,20 @@ func main() {
 	var controllerClass string
 	var enableLeaderElection bool
 	var loglevel string
+	var namespace string
 	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.BoolVar(&enableLeaderElection, "enable-leader-election", false,
 		"Enable leader election for controller manager. "+
 			"Enabling this will ensure there is only one active controller manager.")
 	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")
 	flag.Parse()
 
 	var lvl zapcore.Level
 	err := lvl.UnmarshalText([]byte(loglevel))
 	if err != nil {
-		setupLog.Error(err, "error unmarshaling loglevel")
+		setupLog.Error(err, "error unmarshalling loglevel")
 		os.Exit(1)
 	}
 	logger := zap.New(zap.Level(lvl))
@@ -69,6 +71,7 @@ func main() {
 		Port:               9443,
 		LeaderElection:     enableLeaderElection,
 		LeaderElectionID:   "external-secrets-controller",
+		Namespace:          namespace,
 	})
 	if err != nil {
 		setupLog.Error(err, "unable to start manager")

+ 42 - 102
pkg/controllers/externalsecret/externalsecret_controller.go

@@ -16,9 +16,6 @@ package externalsecret
 
 import (
 	"context"
-
-	// nolint
-	"crypto/md5"
 	"fmt"
 	"time"
 
@@ -38,9 +35,8 @@ import (
 
 	// Loading registered providers.
 	_ "github.com/external-secrets/external-secrets/pkg/provider/register"
-	schema "github.com/external-secrets/external-secrets/pkg/provider/schema"
-	"github.com/external-secrets/external-secrets/pkg/template"
-	utils "github.com/external-secrets/external-secrets/pkg/utils"
+	"github.com/external-secrets/external-secrets/pkg/provider/schema"
+	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
 const (
@@ -54,10 +50,12 @@ const (
 	errStoreRef              = "could not get store reference"
 	errStoreProvider         = "could not get store provider"
 	errStoreClient           = "could not get provider client"
+	errGetExistingSecret     = "could not get existing secret: %w"
 	errCloseStoreClient      = "could not close provider client"
 	errSetCtrlReference      = "could not set ExternalSecret controller reference: %w"
 	errFetchTplFrom          = "error fetching templateFrom data: %w"
 	errGetSecretData         = "could not get secret data from provider: %w"
+	errApplyTemplate         = "could not apply template: %w"
 	errExecTpl               = "could not execute template: %w"
 	errPolicyMergeNotFound   = "the desired secret %s was not found. With creationPolicy=Merge the secret won't be created"
 	errPolicyMergeGetSecret  = "unable to get secret %s: %w"
@@ -126,7 +124,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 
 	// check if store should be handled by this controller instance
 	if !shouldProcessStore(store, r.ControllerClass) {
-		log.Info("skippig unmanaged store")
+		log.Info("skipping unmanaged store")
 		return ctrl.Result{}, nil
 	}
 
@@ -147,7 +145,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 	}
 
 	defer func() {
-		err = secretClient.Close()
+		err = secretClient.Close(ctx)
 		if err != nil {
 			log.Error(err, errCloseStoreClient)
 		}
@@ -158,11 +156,27 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 		refreshInt = externalSecret.Spec.RefreshInterval.Duration
 	}
 
+	// Target Secret Name should default to the ExternalSecret name if not explicitly specified
+	secretName := externalSecret.Spec.Target.Name
+	if secretName == "" {
+		secretName = externalSecret.ObjectMeta.Name
+	}
+
+	// fetch external secret, we need to ensure that it exists, and it's hashmap corresponds
+	var existingSecret v1.Secret
+	err = r.Get(ctx, types.NamespacedName{
+		Name:      secretName,
+		Namespace: externalSecret.Namespace,
+	}, &existingSecret)
+	if err != nil && !apierrors.IsNotFound(err) {
+		log.Error(err, errGetExistingSecret)
+	}
+
 	// refresh should be skipped if
 	// 1. resource generation hasn't changed
 	// 2. refresh interval is 0
 	// 3. if we're still within refresh-interval
-	if !shouldRefresh(externalSecret) {
+	if !shouldRefresh(externalSecret) && isSecretValid(existingSecret) {
 		log.V(1).Info("skipping refresh", "rv", getResourceVersion(externalSecret))
 		return ctrl.Result{RequeueAfter: refreshInt}, nil
 	}
@@ -176,7 +190,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 
 	secret := &v1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      externalSecret.Spec.Target.Name,
+			Name:      secretName,
 			Namespace: externalSecret.Namespace,
 		},
 		Immutable: &externalSecret.Spec.Target.Immutable,
@@ -190,43 +204,21 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 				return fmt.Errorf(errSetCtrlReference, err)
 			}
 		}
-		mergeMetadata(secret, externalSecret)
-		var tplMap map[string][]byte
-		var dataMap map[string][]byte
 
-		// get data
-		dataMap, err = r.getProviderSecretData(ctx, secretClient, &externalSecret)
+		dataMap, err := r.getProviderSecretData(ctx, secretClient, &externalSecret)
 		if err != nil {
 			return fmt.Errorf(errGetSecretData, err)
 		}
 
-		// no template: copy data and return
-		if externalSecret.Spec.Target.Template == nil {
-			for k, v := range dataMap {
-				secret.Data[k] = v
-			}
-			return nil
-		}
-
-		// template: fetch & execute templates
-		tplMap, err = r.getTemplateData(ctx, &externalSecret)
+		err = r.applyTemplate(ctx, &externalSecret, secret, dataMap)
 		if err != nil {
-			return fmt.Errorf(errFetchTplFrom, err)
-		}
-		// override templateFrom data with template data
-		for k, v := range externalSecret.Spec.Target.Template.Data {
-			tplMap[k] = []byte(v)
+			return fmt.Errorf(errApplyTemplate, err)
 		}
 
-		log.V(1).Info("found template data", "tpl_data", tplMap)
-		err = template.Execute(tplMap, dataMap, secret)
-		if err != nil {
-			return fmt.Errorf(errExecTpl, err)
-		}
 		return nil
 	}
 
-	//nolint
+	// nolint
 	switch externalSecret.Spec.Target.CreationPolicy {
 	case esv1alpha1.Merge:
 		err = patchSecret(ctx, r.Client, r.Scheme, secret, mutationFunc)
@@ -273,7 +265,7 @@ func patchSecret(ctx context.Context, c client.Client, scheme *runtime.Scheme, s
 	// https://github.com/kubernetes-sigs/controller-runtime/issues/526
 	// https://github.com/kubernetes-sigs/controller-runtime/issues/1517
 	// https://github.com/kubernetes/kubernetes/issues/80609
-	// we need to manually set it befor doing a Patch() as it depends on the GVK
+	// we need to manually set it before doing a Patch() as it depends on the GVK
 	gvks, unversioned, err := scheme.ObjectKinds(secret)
 	if err != nil {
 		return err
@@ -308,12 +300,10 @@ func hashMeta(m metav1.ObjectMeta) string {
 		annotations map[string]string
 		labels      map[string]string
 	}
-	h := md5.New() //nolint
-	_, _ = h.Write([]byte(fmt.Sprintf("%v", meta{
+	return utils.ObjectHash(meta{
 		annotations: m.Annotations,
 		labels:      m.Labels,
-	})))
-	return fmt.Sprintf("%x", h.Sum(nil))
+	})
 }
 
 func shouldRefresh(es esv1alpha1.ExternalSecret) bool {
@@ -321,6 +311,7 @@ func shouldRefresh(es esv1alpha1.ExternalSecret) bool {
 	if es.Status.SyncedResourceVersion != getResourceVersion(es) {
 		return true
 	}
+
 	// skip refresh if refresh interval is 0
 	if es.Spec.RefreshInterval.Duration == 0 && es.Status.SyncedResourceVersion != "" {
 		return false
@@ -339,7 +330,6 @@ func shouldReconcile(es esv1alpha1.ExternalSecret) bool {
 }
 
 func hasSyncedCondition(es esv1alpha1.ExternalSecret) bool {
-
 	for _, condition := range es.Status.Conditions {
 		if condition.Reason == "SecretSynced" {
 			return true
@@ -348,24 +338,18 @@ func hasSyncedCondition(es esv1alpha1.ExternalSecret) bool {
 	return false
 }
 
-// we do not want to force-override the label/annotations
-// and only copy the necessary key/value pairs.
-func mergeMetadata(secret *v1.Secret, externalSecret esv1alpha1.ExternalSecret) {
-	if secret.ObjectMeta.Labels == nil {
-		secret.ObjectMeta.Labels = make(map[string]string)
-	}
-	if secret.ObjectMeta.Annotations == nil {
-		secret.ObjectMeta.Annotations = make(map[string]string)
+// isSecretValid checks if the secret exists, and it's data is consistent with the calculated hash.
+func isSecretValid(existingSecret v1.Secret) bool {
+	// if target secret doesn't exist, or annotations as not set, we need to refresh
+	if existingSecret.UID == "" || existingSecret.Annotations == nil {
+		return false
 	}
-	if externalSecret.Spec.Target.Template == nil {
-		utils.MergeStringMap(secret.ObjectMeta.Labels, externalSecret.ObjectMeta.Labels)
-		utils.MergeStringMap(secret.ObjectMeta.Annotations, externalSecret.ObjectMeta.Annotations)
-		return
+
+	// if the calculated hash is different from the calculation, then it's invalid
+	if existingSecret.Annotations[esv1alpha1.AnnotationDataHash] != utils.ObjectHash(existingSecret.Data) {
+		return false
 	}
-	// if template is defined: use those labels/annotations
-	secret.Type = externalSecret.Spec.Target.Template.Type
-	utils.MergeStringMap(secret.ObjectMeta.Labels, externalSecret.Spec.Target.Template.Metadata.Labels)
-	utils.MergeStringMap(secret.ObjectMeta.Annotations, externalSecret.Spec.Target.Template.Metadata.Annotations)
+	return true
 }
 
 // getStore returns the store with the provided ExternalSecret.
@@ -419,50 +403,6 @@ func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient p
 	return providerData, nil
 }
 
-func (r *Reconciler) getTemplateData(ctx context.Context, externalSecret *esv1alpha1.ExternalSecret) (map[string][]byte, error) {
-	out := make(map[string][]byte)
-	if externalSecret.Spec.Target.Template == nil {
-		return out, nil
-	}
-	for _, tpl := range externalSecret.Spec.Target.Template.TemplateFrom {
-		if tpl.ConfigMap != nil {
-			var cm v1.ConfigMap
-			err := r.Client.Get(ctx, types.NamespacedName{
-				Name:      tpl.ConfigMap.Name,
-				Namespace: externalSecret.Namespace,
-			}, &cm)
-			if err != nil {
-				return nil, err
-			}
-			for _, k := range tpl.ConfigMap.Items {
-				val, ok := cm.Data[k.Key]
-				if !ok {
-					return nil, fmt.Errorf(errTplCMMissingKey, tpl.ConfigMap.Name, k.Key)
-				}
-				out[k.Key] = []byte(val)
-			}
-		}
-		if tpl.Secret != nil {
-			var sec v1.Secret
-			err := r.Client.Get(ctx, types.NamespacedName{
-				Name:      tpl.Secret.Name,
-				Namespace: externalSecret.Namespace,
-			}, &sec)
-			if err != nil {
-				return nil, err
-			}
-			for _, k := range tpl.Secret.Items {
-				val, ok := sec.Data[k.Key]
-				if !ok {
-					return nil, fmt.Errorf(errTplSecMissingKey, tpl.Secret.Name, k.Key)
-				}
-				out[k.Key] = val
-			}
-		}
-	}
-	return out, nil
-}
-
 // SetupWithManager returns a new controller builder that will be started by the provided Manager.
 func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewControllerManagedBy(mgr).

+ 157 - 0
pkg/controllers/externalsecret/externalsecret_controller_template.go

@@ -0,0 +1,157 @@
+/*
+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 externalsecret
+
+import (
+	"context"
+	"fmt"
+
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+
+	// Loading registered providers.
+	_ "github.com/external-secrets/external-secrets/pkg/provider/register"
+	"github.com/external-secrets/external-secrets/pkg/template"
+	utils "github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+// merge template in the following order:
+// * template.Data (highest precedence)
+// * template.templateFrom
+// * secret via es.data or es.dataFrom.
+func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1alpha1.ExternalSecret, secret *v1.Secret, dataMap map[string][]byte) error {
+	mergeMetadata(secret, es)
+
+	// no template: copy data and return
+	if es.Spec.Target.Template == nil {
+		secret.Data = dataMap
+		secret.Annotations[esv1alpha1.AnnotationDataHash] = utils.ObjectHash(secret.Data)
+		return nil
+	}
+
+	// fetch templates defined in template.templateFrom
+	tplMap, err := r.getTemplateData(ctx, es)
+	if err != nil {
+		return fmt.Errorf(errFetchTplFrom, err)
+	}
+
+	// explicitly defined template.Data takes precedence over templateFrom
+	for k, v := range es.Spec.Target.Template.Data {
+		tplMap[k] = []byte(v)
+	}
+	r.Log.V(1).Info("found template data", "tpl_data", tplMap)
+
+	err = template.Execute(tplMap, dataMap, secret)
+	if err != nil {
+		return fmt.Errorf(errExecTpl, err)
+	}
+
+	// if no data was provided by template fallback
+	// to value from the provider
+	if len(es.Spec.Target.Template.Data) == 0 {
+		for k, v := range dataMap {
+			secret.Data[k] = v
+		}
+	}
+	secret.Annotations[esv1alpha1.AnnotationDataHash] = utils.ObjectHash(secret.Data)
+
+	return nil
+}
+
+// we do not want to force-override the label/annotations
+// and only copy the necessary key/value pairs.
+func mergeMetadata(secret *v1.Secret, externalSecret *esv1alpha1.ExternalSecret) {
+	if secret.ObjectMeta.Labels == nil {
+		secret.ObjectMeta.Labels = make(map[string]string)
+	}
+	if secret.ObjectMeta.Annotations == nil {
+		secret.ObjectMeta.Annotations = make(map[string]string)
+	}
+	if externalSecret.Spec.Target.Template == nil {
+		utils.MergeStringMap(secret.ObjectMeta.Labels, externalSecret.ObjectMeta.Labels)
+		utils.MergeStringMap(secret.ObjectMeta.Annotations, externalSecret.ObjectMeta.Annotations)
+		return
+	}
+	// if template is defined: use those labels/annotations
+	secret.Type = externalSecret.Spec.Target.Template.Type
+	utils.MergeStringMap(secret.ObjectMeta.Labels, externalSecret.Spec.Target.Template.Metadata.Labels)
+	utils.MergeStringMap(secret.ObjectMeta.Annotations, externalSecret.Spec.Target.Template.Metadata.Annotations)
+}
+
+func (r *Reconciler) getTemplateData(ctx context.Context, externalSecret *esv1alpha1.ExternalSecret) (map[string][]byte, error) {
+	out := make(map[string][]byte)
+	if externalSecret.Spec.Target.Template == nil {
+		return out, nil
+	}
+	for _, tpl := range externalSecret.Spec.Target.Template.TemplateFrom {
+		err := mergeConfigMap(ctx, r.Client, externalSecret, tpl, out)
+		if err != nil {
+			return nil, err
+		}
+		err = mergeSecret(ctx, r.Client, externalSecret, tpl, out)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return out, nil
+}
+
+func mergeConfigMap(ctx context.Context, k8sClient client.Client, es *esv1alpha1.ExternalSecret, tpl esv1alpha1.TemplateFrom, out map[string][]byte) error {
+	if tpl.ConfigMap == nil {
+		return nil
+	}
+
+	var cm v1.ConfigMap
+	err := k8sClient.Get(ctx, types.NamespacedName{
+		Name:      tpl.ConfigMap.Name,
+		Namespace: es.Namespace,
+	}, &cm)
+	if err != nil {
+		return err
+	}
+	for _, k := range tpl.ConfigMap.Items {
+		val, ok := cm.Data[k.Key]
+		if !ok {
+			return fmt.Errorf(errTplCMMissingKey, tpl.ConfigMap.Name, k.Key)
+		}
+		out[k.Key] = []byte(val)
+	}
+	return nil
+}
+
+func mergeSecret(ctx context.Context, k8sClient client.Client, es *esv1alpha1.ExternalSecret, tpl esv1alpha1.TemplateFrom, out map[string][]byte) error {
+	if tpl.Secret == nil {
+		return nil
+	}
+	var sec v1.Secret
+	err := k8sClient.Get(ctx, types.NamespacedName{
+		Name:      tpl.Secret.Name,
+		Namespace: es.Namespace,
+	}, &sec)
+	if err != nil {
+		return err
+	}
+	for _, k := range tpl.Secret.Items {
+		val, ok := sec.Data[k.Key]
+		if !ok {
+			return fmt.Errorf(errTplSecMissingKey, tpl.Secret.Name, k.Key)
+		}
+		out[k.Key] = val
+	}
+	return nil
+}

+ 268 - 51
pkg/controllers/externalsecret/externalsecret_controller_test.go

@@ -16,6 +16,8 @@ package externalsecret
 import (
 	"context"
 	"fmt"
+	"os"
+	"strconv"
 	"time"
 
 	. "github.com/onsi/ginkgo"
@@ -59,6 +61,74 @@ type testCase struct {
 
 type testTweaks func(*testCase)
 
+var _ = Describe("Kind=secret existence logic", func() {
+	type testCase struct {
+		Name           string
+		Input          v1.Secret
+		ExpectedOutput bool
+	}
+	tests := []testCase{
+		{
+			Name:           "Should not be valid in case of missing uid",
+			Input:          v1.Secret{},
+			ExpectedOutput: false,
+		},
+		{
+			Name: "A nil annotation should not be valid",
+			Input: v1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					UID:         "xxx",
+					Annotations: map[string]string{},
+				},
+			},
+			ExpectedOutput: false,
+		},
+		{
+			Name: "A nil annotation should not be valid",
+			Input: v1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					UID:         "xxx",
+					Annotations: map[string]string{},
+				},
+			},
+			ExpectedOutput: false,
+		},
+		{
+			Name: "An invalid annotation hash should not be valid",
+			Input: v1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					UID: "xxx",
+					Annotations: map[string]string{
+						esv1alpha1.AnnotationDataHash: "xxxxxx",
+					},
+				},
+			},
+			ExpectedOutput: false,
+		},
+		{
+			Name: "A valid config map should return true",
+			Input: v1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					UID: "xxx",
+					Annotations: map[string]string{
+						esv1alpha1.AnnotationDataHash: "caa0155759a6a9b3b6ada5a6883ee2bb",
+					},
+				},
+				Data: map[string][]byte{
+					"foo": []byte("value1"),
+					"bar": []byte("value2"),
+				},
+			},
+			ExpectedOutput: true,
+		},
+	}
+
+	for _, tt := range tests {
+		It(tt.Name, func() {
+			Expect(isSecretValid(tt.Input)).To(BeEquivalentTo(tt.ExpectedOutput))
+		})
+	}
+})
 var _ = Describe("ExternalSecret controller", func() {
 	const (
 		ExternalSecretName             = "test-es"
@@ -68,6 +138,13 @@ var _ = Describe("ExternalSecret controller", func() {
 
 	var ExternalSecretNamespace string
 
+	// if we are in debug and need to increase the timeout for testing, we can do so by using an env var
+	if customTimeout := os.Getenv("TEST_CUSTOM_TIMEOUT_SEC"); customTimeout != "" {
+		if t, err := strconv.Atoi(customTimeout); err == nil {
+			timeout = time.Second * time.Duration(t)
+		}
+	}
+
 	BeforeEach(func() {
 		var err error
 		ExternalSecretNamespace, err = CreateNamespace("test-ns", k8sClient)
@@ -158,24 +235,32 @@ var _ = Describe("ExternalSecret controller", func() {
 		}
 		fakeProvider.WithGetSecret([]byte(secretVal), nil)
 		tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
-			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
-			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
-			Eventually(func() bool {
-				Expect(syncCallsTotal.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
-				return metric.GetCounter().GetValue() == 1.0
-			}, timeout, interval).Should(BeTrue())
-
 			// check value
 			Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
 
 			// check labels & annotations
 			Expect(secret.ObjectMeta.Labels).To(BeEquivalentTo(es.ObjectMeta.Labels))
-			Expect(secret.ObjectMeta.Annotations).To(BeEquivalentTo(es.ObjectMeta.Annotations))
+			for k, v := range es.ObjectMeta.Annotations {
+				Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v))
+			}
 			// ownerRef must not not be set!
 			Expect(hasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretName)).To(BeTrue())
 		}
 	}
 
+	checkPrometheusCounters := func(tc *testCase) {
+		const secretVal = "someValue"
+		fakeProvider.WithGetSecret([]byte(secretVal), nil)
+		tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
+			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
+			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
+			Eventually(func() bool {
+				Expect(syncCallsTotal.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
+				return metric.GetCounter().GetValue() == 1.0
+			}, timeout, interval).Should(BeTrue())
+		}
+	}
+
 	// merge with existing secret using creationPolicy=Merge
 	// it should NOT have a ownerReference
 	// metadata.managedFields with the correct owner should be added to the secret
@@ -198,23 +283,22 @@ var _ = Describe("ExternalSecret controller", func() {
 
 		fakeProvider.WithGetSecret([]byte(secretVal), nil)
 		tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
-			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
-			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
-			Eventually(func() bool {
-				Expect(syncCallsTotal.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
-				return metric.GetCounter().GetValue() == 1.0
-			}, timeout, interval).Should(BeTrue())
-
 			// check value
 			Expect(string(secret.Data[existingKey])).To(Equal(existingVal))
 			Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
 
 			// check labels & annotations
 			Expect(secret.ObjectMeta.Labels).To(BeEquivalentTo(es.ObjectMeta.Labels))
-			Expect(secret.ObjectMeta.Annotations).To(BeEquivalentTo(es.ObjectMeta.Annotations))
+			for k, v := range es.ObjectMeta.Annotations {
+				Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v))
+			}
 			Expect(hasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretName)).To(BeFalse())
 			Expect(secret.ObjectMeta.ManagedFields).To(HaveLen(2))
-			Expect(hasFieldOwnership(secret.ObjectMeta, "external-secrets", "{\"f:data\":{\"f:targetProperty\":{}},\"f:immutable\":{}}")).To(BeTrue())
+			Expect(hasFieldOwnership(
+				secret.ObjectMeta,
+				"external-secrets",
+				fmt.Sprintf("{\"f:data\":{\"f:targetProperty\":{}},\"f:immutable\":{},\"f:metadata\":{\"f:annotations\":{\"f:%s\":{}}}}", esv1alpha1.AnnotationDataHash)),
+			).To(BeTrue())
 			Expect(hasFieldOwnership(secret.ObjectMeta, "fake.manager", "{\"f:data\":{\".\":{},\"f:pre-existing-key\":{}},\"f:type\":{}}")).To(BeTrue())
 		}
 	}
@@ -313,20 +397,15 @@ var _ = Describe("ExternalSecret controller", func() {
 		}
 		fakeProvider.WithGetSecret([]byte(secretVal), nil)
 		tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
-			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
-			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
-			Eventually(func() bool {
-				Expect(syncCallsTotal.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
-				return metric.GetCounter().GetValue() == 1.0
-			}, timeout, interval).Should(BeTrue())
-
 			// check values
 			Expect(string(secret.Data[targetProp])).To(Equal(expectedSecretVal))
 			Expect(string(secret.Data[tplStaticKey])).To(Equal(tplStaticVal))
 
 			// labels/annotations should be taken from the template
 			Expect(secret.ObjectMeta.Labels).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Labels))
-			Expect(secret.ObjectMeta.Annotations).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Annotations))
+			for k, v := range es.Spec.Target.Template.Metadata.Annotations {
+				Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v))
+			}
 		}
 	}
 
@@ -341,17 +420,29 @@ var _ = Describe("ExternalSecret controller", func() {
 		const tplStaticKey = "tplstatickey"
 		const tplStaticVal = "tplstaticvalue"
 		const tplFromCMName = "template-cm"
+		const tplFromSecretName = "template-secret"
 		const tplFromKey = "tpl-from-key"
+		const tplFromSecKey = "tpl-from-sec-key"
 		const tplFromVal = "tpl-from-value: {{ .targetProperty | toString }} // {{ .bar | toString }}"
+		const tplFromSecVal = "tpl-from-sec-value: {{ .targetProperty | toString }} // {{ .bar | toString }}"
 		Expect(k8sClient.Create(context.Background(), &v1.ConfigMap{
 			ObjectMeta: metav1.ObjectMeta{
-				Name:      "template-cm",
+				Name:      tplFromCMName,
 				Namespace: ExternalSecretNamespace,
 			},
 			Data: map[string]string{
 				tplFromKey: tplFromVal,
 			},
 		})).To(Succeed())
+		Expect(k8sClient.Create(context.Background(), &v1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      tplFromSecretName,
+				Namespace: ExternalSecretNamespace,
+			},
+			Data: map[string][]byte{
+				tplFromSecKey: []byte(tplFromSecVal),
+			},
+		})).To(Succeed())
 		tc.externalSecret.Spec.Target.Template = &esv1alpha1.ExternalSecretTemplate{
 			Metadata: esv1alpha1.ExternalSecretTemplateMetadata{},
 			Type:     v1.SecretTypeOpaque,
@@ -366,6 +457,16 @@ var _ = Describe("ExternalSecret controller", func() {
 						},
 					},
 				},
+				{
+					Secret: &esv1alpha1.TemplateRef{
+						Name: tplFromSecretName,
+						Items: []esv1alpha1.TemplateRefItem{
+							{
+								Key: tplFromSecKey,
+							},
+						},
+					},
+				},
 			},
 			Data: map[string]string{
 				// this should be the data value, not dataFrom
@@ -392,6 +493,7 @@ var _ = Describe("ExternalSecret controller", func() {
 			Expect(string(secret.Data[tplStaticKey])).To(Equal(tplStaticVal))
 			Expect(string(secret.Data["bar"])).To(Equal("value from map: map-bar-value"))
 			Expect(string(secret.Data[tplFromKey])).To(Equal("tpl-from-value: someValue // map-bar-value"))
+			Expect(string(secret.Data[tplFromSecKey])).To(Equal("tpl-from-sec-value: someValue // map-bar-value"))
 		}
 	}
 
@@ -420,7 +522,12 @@ var _ = Describe("ExternalSecret controller", func() {
 
 			// labels/annotations should be taken from the template
 			Expect(secret.ObjectMeta.Labels).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Labels))
-			Expect(secret.ObjectMeta.Annotations).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Annotations))
+
+			// a secret will always have some extra annotations (i.e. hashmap check), so we only check for specific
+			// source annotations
+			for k, v := range es.Spec.Target.Template.Metadata.Annotations {
+				Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v))
+			}
 
 			cleanEs := tc.externalSecret.DeepCopy()
 
@@ -447,7 +554,31 @@ var _ = Describe("ExternalSecret controller", func() {
 
 			// also check labels/annotations have been updated
 			Expect(secret.ObjectMeta.Labels).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Labels))
-			Expect(secret.ObjectMeta.Annotations).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Annotations))
+			for k, v := range es.Spec.Target.Template.Metadata.Annotations {
+				Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v))
+			}
+		}
+	}
+
+	onlyMetadataFromTemplate := func(tc *testCase) {
+		const secretVal = "someValue"
+		tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Second}
+		tc.externalSecret.Spec.Target.Template = &esv1alpha1.ExternalSecretTemplate{
+			Metadata: esv1alpha1.ExternalSecretTemplateMetadata{
+				Labels:      map[string]string{"foo": "bar"},
+				Annotations: map[string]string{"foo": "bar"},
+			},
+		}
+		fakeProvider.WithGetSecret([]byte(secretVal), nil)
+		tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
+			// check values
+			Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
+
+			// labels/annotations should be taken from the template
+			Expect(secret.ObjectMeta.Labels).To(BeEquivalentTo(es.Spec.Target.Template.Metadata.Labels))
+			for k, v := range es.Spec.Target.Template.Metadata.Annotations {
+				Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v))
+			}
 		}
 	}
 
@@ -459,13 +590,6 @@ var _ = Describe("ExternalSecret controller", func() {
 		fakeProvider.WithGetSecret([]byte(secretVal), nil)
 		tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Second}
 		tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
-			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
-			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
-			Eventually(func() bool {
-				Expect(syncCallsTotal.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
-				return metric.GetCounter().GetValue() == 1.0
-			}, timeout, interval).Should(BeTrue())
-
 			// check values
 			Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
 
@@ -494,13 +618,6 @@ var _ = Describe("ExternalSecret controller", func() {
 		fakeProvider.WithGetSecret([]byte(secretVal), nil)
 		tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: 0}
 		tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
-			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
-			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
-			Eventually(func() bool {
-				Expect(syncCallsTotal.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
-				return metric.GetCounter().GetValue() == 1.0
-			}, timeout, interval).Should(BeTrue())
-
 			// check values
 			Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
 
@@ -537,19 +654,40 @@ var _ = Describe("ExternalSecret controller", func() {
 			"bar": []byte("map-bar-value"),
 		}, nil)
 		tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
-			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
-			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
-			Eventually(func() bool {
-				Expect(syncCallsTotal.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
-				return metric.GetCounter().GetValue() == 1.0
-			}, timeout, interval).Should(BeTrue())
-
 			// check values
 			Expect(string(secret.Data["foo"])).To(Equal("map-foo-value"))
 			Expect(string(secret.Data["bar"])).To(Equal("map-bar-value"))
 		}
 	}
 
+	// with dataFrom and using a template
+	// should be put into the secret
+	syncWithDataFromTemplate := func(tc *testCase) {
+		tc.externalSecret.Spec.Data = nil
+		tc.externalSecret.Spec.Target = esv1alpha1.ExternalSecretTarget{
+			Name: ExternalSecretTargetSecretName,
+			Template: &esv1alpha1.ExternalSecretTemplate{
+				Type: v1.SecretTypeTLS,
+			},
+		}
+
+		tc.externalSecret.Spec.DataFrom = []esv1alpha1.ExternalSecretDataRemoteRef{
+			{
+				Key: remoteKey,
+			},
+		}
+		fakeProvider.WithGetSecretMap(map[string][]byte{
+			"tls.crt": []byte("map-foo-value"),
+			"tls.key": []byte("map-bar-value"),
+		}, nil)
+		tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
+			Expect(secret.Type).To(Equal(v1.SecretTypeTLS))
+			// check values
+			Expect(string(secret.Data["tls.crt"])).To(Equal("map-foo-value"))
+			Expect(string(secret.Data["tls.key"])).To(Equal("map-bar-value"))
+		}
+	}
+
 	// when a provider errors in a GetSecret call
 	// a error condition must be set.
 	providerErrCondition := func(tc *testCase) {
@@ -660,6 +798,80 @@ var _ = Describe("ExternalSecret controller", func() {
 		}
 	}
 
+	// When the ownership is set to owner, and we delete a dependent child kind=secret
+	// it should be recreated without waiting for refresh interval
+	checkDeletion := func(tc *testCase) {
+		const secretVal = "someValue"
+		fakeProvider.WithGetSecret([]byte(secretVal), nil)
+		tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Minute * 10}
+		tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
+
+			// check values
+			oldUID := secret.UID
+			Expect(oldUID).NotTo(BeEmpty())
+
+			// delete the related config
+			Expect(k8sClient.Delete(context.TODO(), secret))
+
+			var newSecret v1.Secret
+			secretLookupKey := types.NamespacedName{
+				Name:      ExternalSecretTargetSecretName,
+				Namespace: ExternalSecretNamespace,
+			}
+			Eventually(func() bool {
+				err := k8sClient.Get(context.Background(), secretLookupKey, &newSecret)
+				if err != nil {
+					return false
+				}
+				// new secret should be a new, recreated object with a different UID
+				return newSecret.UID != oldUID
+			}, timeout, interval).Should(BeTrue())
+		}
+	}
+
+	// Checks that secret annotation has been written based on the data
+	checkSecretDataHashAnnotation := func(tc *testCase) {
+		const secretVal = "someValue"
+		fakeProvider.WithGetSecret([]byte(secretVal), nil)
+		tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
+			Expect(secret.Annotations[esv1alpha1.AnnotationDataHash]).To(Equal("9d30b95ca81e156f9454b5ef3bfcc6ee"))
+		}
+	}
+
+	// When we amend the created kind=secret, refresh operation should be run again regardless of refresh interval
+	checkSecretDataHashAnnotationChange := func(tc *testCase) {
+		fakeData := map[string][]byte{
+			"targetProperty": []byte("map-foo-value"),
+		}
+		fakeProvider.WithGetSecretMap(fakeData, nil)
+		tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Minute * 10}
+		tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
+			oldHash := secret.Annotations[esv1alpha1.AnnotationDataHash]
+			oldResourceVersion := secret.ResourceVersion
+			Expect(oldHash).NotTo(BeEmpty())
+
+			cleanSecret := secret.DeepCopy()
+			secret.Data["new"] = []byte("value")
+			secret.ObjectMeta.Annotations[esv1alpha1.AnnotationDataHash] = "thisiswronghash"
+			Expect(k8sClient.Patch(context.Background(), secret, client.MergeFrom(cleanSecret))).To(Succeed())
+
+			var refreshedSecret v1.Secret
+			secretLookupKey := types.NamespacedName{
+				Name:      ExternalSecretTargetSecretName,
+				Namespace: ExternalSecretNamespace,
+			}
+			Eventually(func() bool {
+				err := k8sClient.Get(context.Background(), secretLookupKey, &refreshedSecret)
+				if err != nil {
+					return false
+				}
+				// refreshed secret should have a different generation (sign that it was updated), but since
+				// the secret source is the same (not changed), the hash should be reverted to an old value
+				return refreshedSecret.ResourceVersion != oldResourceVersion && refreshedSecret.Annotations[esv1alpha1.AnnotationDataHash] == oldHash
+			}, timeout, interval).Should(BeTrue())
+		}
+	}
+
 	DescribeTable("When reconciling an ExternalSecret",
 		func(tweaks ...testTweaks) {
 			tc := makeDefaultTestcase()
@@ -696,16 +908,22 @@ var _ = Describe("ExternalSecret controller", func() {
 				tc.checkSecret(createdES, syncedSecret)
 			}
 		},
+		Entry("should recreate deleted secret", checkDeletion),
+		Entry("should create proper hash annotation for the external secret", checkSecretDataHashAnnotation),
+		Entry("should refresh when the hash annotation doesn't correspond to secret data", checkSecretDataHashAnnotationChange),
 		Entry("should set the condition eventually", syncLabelsAnnotations),
+		Entry("should set prometheus counters", checkPrometheusCounters),
 		Entry("should merge with existing secret using creationPolicy=Merge", mergeWithSecret),
-		Entry("should error if sceret doesn't exist when using creationPolicy=Merge", mergeWithSecretErr),
+		Entry("should error if secret doesn't exist when using creationPolicy=Merge", mergeWithSecretErr),
 		Entry("should not resolve conflicts with creationPolicy=Merge", mergeWithConflict),
 		Entry("should sync with template", syncWithTemplate),
 		Entry("should sync template with correct value precedence", syncWithTemplatePrecedence),
 		Entry("should refresh secret from template", refreshWithTemplate),
+		Entry("should be able to use only metadata from template", onlyMetadataFromTemplate),
 		Entry("should refresh secret value when provider secret changes", refreshSecretValue),
 		Entry("should not refresh secret value when provider secret changes but refreshInterval is zero", refreshintervalZero),
 		Entry("should fetch secret using dataFrom", syncWithDataFrom),
+		Entry("should fetch secret using dataFrom and a template", syncWithDataFromTemplate),
 		Entry("should set error condition when provider errors", providerErrCondition),
 		Entry("should set an error condition when store does not exist", storeMissingErrCondition),
 		Entry("should set an error condition when store provider constructor fails", storeConstructErrCondition),
@@ -722,7 +940,6 @@ var _ = Describe("ExternalSecret refresh logic", func() {
 				},
 			})).To(BeTrue())
 		})
-
 		It("should refresh when labels change", func() {
 			es := esv1alpha1.ExternalSecret{
 				ObjectMeta: metav1.ObjectMeta{

+ 35 - 0
pkg/provider/alibaba/fake/fake.go

@@ -0,0 +1,35 @@
+/*
+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 fake
+
+import (
+	kmssdk "github.com/aliyun/alibaba-cloud-sdk-go/services/kms"
+)
+
+type AlibabaMockClient struct {
+	getSecretValue func(request *kmssdk.GetSecretValueRequest) (response *kmssdk.GetSecretValueResponse, err error)
+}
+
+func (mc *AlibabaMockClient) GetSecretValue(*kmssdk.GetSecretValueRequest) (result *kmssdk.GetSecretValueResponse, err error) {
+	return mc.getSecretValue(&kmssdk.GetSecretValueRequest{})
+}
+
+func (mc *AlibabaMockClient) WithValue(in *kmssdk.GetSecretValueRequest, val *kmssdk.GetSecretValueResponse, err error) {
+	if mc != nil {
+		mc.getSecretValue = func(paramIn *kmssdk.GetSecretValueRequest) (*kmssdk.GetSecretValueResponse, error) {
+			return val, err
+		}
+	}
+}

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

@@ -0,0 +1,193 @@
+/*
+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 alibaba
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	kmssdk "github.com/aliyun/alibaba-cloud-sdk-go/services/kms"
+	"github.com/tidwall/gjson"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"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"
+)
+
+const (
+	errAlibabaClient                           = "cannot setup new Alibaba client: %w"
+	errAlibabaCredSecretName                   = "invalid Alibaba SecretStore resource: missing Alibaba APIKey"
+	errUninitalizedAlibabaProvider             = "provider Alibaba is not initialized"
+	errInvalidClusterStoreMissingAKIDNamespace = "invalid ClusterStore, missing  AccessKeyID namespace"
+	errInvalidClusterStoreMissingSKNamespace   = "invalid ClusterStore, missing namespace"
+	errFetchAKIDSecret                         = "could not fetch AccessKeyID secret: %w"
+	errMissingSAK                              = "missing AccessSecretKey"
+	errMissingAKID                             = "missing AccessKeyID"
+)
+
+type Client struct {
+	kube      kclient.Client
+	store     *esv1alpha1.AlibabaProvider
+	namespace string
+	storeKind string
+	regionID  string
+	keyID     []byte
+	accessKey []byte
+}
+
+type KeyManagementService struct {
+	Client SMInterface
+}
+
+type SMInterface interface {
+	GetSecretValue(request *kmssdk.GetSecretValueRequest) (response *kmssdk.GetSecretValueResponse, err error)
+}
+
+// setAuth creates a new Alibaba session based on a store.
+func (c *Client) setAuth(ctx context.Context) error {
+	credentialsSecret := &corev1.Secret{}
+	credentialsSecretName := c.store.Auth.SecretRef.AccessKeyID.Name
+	if credentialsSecretName == "" {
+		return fmt.Errorf(errAlibabaCredSecretName)
+	}
+	objectKey := types.NamespacedName{
+		Name:      credentialsSecretName,
+		Namespace: c.namespace,
+	}
+
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if c.storeKind == esv1alpha1.ClusterSecretStoreKind {
+		if c.store.Auth.SecretRef.AccessKeyID.Namespace == nil {
+			return fmt.Errorf(errInvalidClusterStoreMissingAKIDNamespace)
+		}
+		objectKey.Namespace = *c.store.Auth.SecretRef.AccessKeyID.Namespace
+	}
+
+	err := c.kube.Get(ctx, objectKey, credentialsSecret)
+	if err != nil {
+		return fmt.Errorf(errFetchAKIDSecret, err)
+	}
+
+	objectKey = types.NamespacedName{
+		Name:      c.store.Auth.SecretRef.AccessKeySecret.Name,
+		Namespace: c.namespace,
+	}
+	if c.storeKind == esv1alpha1.ClusterSecretStoreKind {
+		if c.store.Auth.SecretRef.AccessKeySecret.Namespace == nil {
+			return fmt.Errorf(errInvalidClusterStoreMissingSKNamespace)
+		}
+		objectKey.Namespace = *c.store.Auth.SecretRef.AccessKeySecret.Namespace
+	}
+	c.keyID = credentialsSecret.Data[c.store.Auth.SecretRef.AccessKeyID.Key]
+	fmt.Println(c.keyID)
+	fmt.Println(c.accessKey)
+	if (c.keyID == nil) || (len(c.keyID) == 0) {
+		return fmt.Errorf(errMissingAKID)
+	}
+	c.accessKey = credentialsSecret.Data[c.store.Auth.SecretRef.AccessKeySecret.Key]
+	if (c.accessKey == nil) || (len(c.accessKey) == 0) {
+		return fmt.Errorf(errMissingSAK)
+	}
+	c.regionID = c.store.RegionID
+	return nil
+}
+
+// GetSecret returns a single secret from the provider.
+func (kms *KeyManagementService) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	if utils.IsNil(kms.Client) {
+		return nil, fmt.Errorf(errUninitalizedAlibabaProvider)
+	}
+	kmsRequest := kmssdk.CreateGetSecretValueRequest()
+	kmsRequest.VersionId = ref.Version
+	kmsRequest.SecretName = ref.Key
+	kmsRequest.SetScheme("https")
+	secretOut, err := kms.Client.GetSecretValue(kmsRequest)
+	if err != nil {
+		return nil, util.SanitizeErr(err)
+	}
+	if ref.Property == "" {
+		if secretOut.SecretData != "" {
+			return []byte(secretOut.SecretData), nil
+		}
+		return nil, fmt.Errorf("invalid secret received. no secret string nor binary for key: %s", ref.Key)
+	}
+	var payload string
+	if secretOut.SecretData != "" {
+		payload = secretOut.SecretData
+	}
+	val := gjson.Get(payload, ref.Property)
+	if !val.Exists() {
+		return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
+	}
+	return []byte(val.String()), nil
+}
+
+// GetSecretMap returns multiple k/v pairs from the provider.
+func (kms *KeyManagementService) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	data, err := kms.GetSecret(ctx, ref)
+	if err != nil {
+		return nil, err
+	}
+	kv := make(map[string]string)
+	err = json.Unmarshal(data, &kv)
+	if err != nil {
+		return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
+	}
+	secretData := make(map[string][]byte)
+	for k, v := range kv {
+		secretData[k] = []byte(v)
+	}
+	return secretData, nil
+}
+
+// NewClient constructs a new secrets client based on the provided store.
+func (kms *KeyManagementService) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
+	storeSpec := store.GetSpec()
+	alibabaSpec := storeSpec.Provider.Alibaba
+	iStore := &Client{
+		kube:      kube,
+		store:     alibabaSpec,
+		namespace: namespace,
+		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
+	}
+	if err := iStore.setAuth(ctx); err != nil {
+		return nil, err
+	}
+	alibabaRegion := iStore.regionID
+	alibabaKeyID := iStore.keyID
+	alibabaSecretKey := iStore.accessKey
+	keyManagementService, err := kmssdk.NewClientWithAccessKey(alibabaRegion, string(alibabaKeyID), string(alibabaSecretKey))
+	if err != nil {
+		return nil, fmt.Errorf(errAlibabaClient, err)
+	}
+	kms.Client = keyManagementService
+	return kms, nil
+}
+
+func (kms *KeyManagementService) Close(ctx context.Context) error {
+	return nil
+}
+
+func init() {
+	schema.Register(&KeyManagementService{}, &esv1alpha1.SecretStoreProvider{
+		Alibaba: &esv1alpha1.AlibabaProvider{},
+	})
+}

+ 197 - 0
pkg/provider/alibaba/kms_test.go

@@ -0,0 +1,197 @@
+/*
+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 alibaba
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/responses"
+	kmssdk "github.com/aliyun/alibaba-cloud-sdk-go/services/kms"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	fakesm "github.com/external-secrets/external-secrets/pkg/provider/alibaba/fake"
+)
+
+const (
+	secretName  = "test-example"
+	secretValue = "value"
+)
+
+type keyManagementServiceTestCase struct {
+	mockClient     *fakesm.AlibabaMockClient
+	apiInput       *kmssdk.GetSecretValueRequest
+	apiOutput      *kmssdk.GetSecretValueResponse
+	ref            *esv1alpha1.ExternalSecretDataRemoteRef
+	apiErr         error
+	expectError    string
+	expectedSecret string
+	// for testing secretmap
+	expectedData map[string][]byte
+}
+
+func makeValidKMSTestCase() *keyManagementServiceTestCase {
+	kmstc := keyManagementServiceTestCase{
+		mockClient:     &fakesm.AlibabaMockClient{},
+		apiInput:       makeValidAPIInput(),
+		ref:            makeValidRef(),
+		apiOutput:      makeValidAPIOutput(),
+		apiErr:         nil,
+		expectError:    "",
+		expectedSecret: "",
+		expectedData:   make(map[string][]byte),
+	}
+	kmstc.mockClient.WithValue(kmstc.apiInput, kmstc.apiOutput, kmstc.apiErr)
+	return &kmstc
+}
+
+func makeValidRef() *esv1alpha1.ExternalSecretDataRemoteRef {
+	return &esv1alpha1.ExternalSecretDataRemoteRef{
+		Key: secretName,
+	}
+}
+
+func makeValidAPIInput() *kmssdk.GetSecretValueRequest {
+	return &kmssdk.GetSecretValueRequest{
+		SecretName: secretName,
+	}
+}
+
+func makeValidAPIOutput() *kmssdk.GetSecretValueResponse {
+	kmsresponse := &kmssdk.GetSecretValueResponse{
+		BaseResponse:      &responses.BaseResponse{},
+		RequestId:         "",
+		SecretName:        secretName,
+		VersionId:         "",
+		CreateTime:        "",
+		SecretData:        secretValue,
+		SecretDataType:    "",
+		AutomaticRotation: "",
+		RotationInterval:  "",
+		NextRotationDate:  "",
+		ExtendedConfig:    "",
+		LastRotationDate:  "",
+		SecretType:        "",
+		VersionStages:     kmssdk.VersionStagesInGetSecretValue{},
+	}
+	return kmsresponse
+}
+
+func makeValidKMSTestCaseCustom(tweaks ...func(kmstc *keyManagementServiceTestCase)) *keyManagementServiceTestCase {
+	kmstc := makeValidKMSTestCase()
+	for _, fn := range tweaks {
+		fn(kmstc)
+	}
+	kmstc.mockClient.WithValue(kmstc.apiInput, kmstc.apiOutput, kmstc.apiErr)
+	return kmstc
+}
+
+var setAPIErr = func(kmstc *keyManagementServiceTestCase) {
+	kmstc.apiErr = fmt.Errorf("oh no")
+	kmstc.expectError = "oh no"
+}
+
+var setNilMockClient = func(kmstc *keyManagementServiceTestCase) {
+	kmstc.mockClient = nil
+	kmstc.expectError = errUninitalizedAlibabaProvider
+}
+
+func TestAlibabaKMSGetSecret(t *testing.T) {
+	secretData := make(map[string]interface{})
+	secretValue := "changedvalue"
+	secretData["payload"] = secretValue
+
+	// good case: default version is set
+	// key is passed in, output is sent back
+	setSecretString := func(kmstc *keyManagementServiceTestCase) {
+		kmstc.apiOutput.SecretName = secretName
+		kmstc.apiOutput.SecretData = secretValue
+		kmstc.expectedSecret = secretValue
+	}
+
+	// good case: custom version set
+	setCustomKey := func(kmstc *keyManagementServiceTestCase) {
+		kmstc.apiOutput.SecretName = "test-example-other"
+		kmstc.ref.Key = "test-example-other"
+		kmstc.apiOutput.SecretData = secretValue
+		kmstc.expectedSecret = secretValue
+	}
+
+	successCases := []*keyManagementServiceTestCase{
+		makeValidKMSTestCaseCustom(setSecretString),
+		makeValidKMSTestCaseCustom(setCustomKey),
+		makeValidKMSTestCaseCustom(setAPIErr),
+		makeValidKMSTestCaseCustom(setNilMockClient),
+	}
+
+	sm := KeyManagementService{}
+	for k, v := range successCases {
+		sm.Client = v.mockClient
+		out, err := sm.GetSecret(context.Background(), *v.ref)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
+		}
+		if string(out) != v.expectedSecret {
+			t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out))
+		}
+	}
+}
+
+func TestGetSecretMap(t *testing.T) {
+	// good case: default version & deserialization
+	setDeserialization := func(kmstc *keyManagementServiceTestCase) {
+		kmstc.apiOutput.SecretName = "foo"
+		kmstc.expectedData["foo"] = []byte("bar")
+		kmstc.apiOutput.SecretData = `{"foo":"bar"}`
+	}
+
+	// bad case: invalid json
+	setInvalidJSON := func(kmstc *keyManagementServiceTestCase) {
+		kmstc.apiOutput.SecretData = "-----------------"
+		kmstc.expectError = "unable to unmarshal secret"
+	}
+
+	successCases := []*keyManagementServiceTestCase{
+		makeValidKMSTestCaseCustom(setDeserialization),
+		makeValidKMSTestCaseCustom(setInvalidJSON),
+		makeValidKMSTestCaseCustom(setNilMockClient),
+		makeValidKMSTestCaseCustom(setAPIErr),
+	}
+
+	sm := KeyManagementService{}
+	for k, v := range successCases {
+		sm.Client = v.mockClient
+		out, err := sm.GetSecretMap(context.Background(), *v.ref)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
+		}
+		if err == nil && !reflect.DeepEqual(out, v.expectedData) {
+			t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedData, out)
+		}
+	}
+}
+
+func ErrorContains(out error, want string) bool {
+	if out == nil {
+		return want == ""
+	}
+	if want == "" {
+		return false
+	}
+	return strings.Contains(out.Error(), want)
+}

+ 1 - 1
pkg/provider/aws/parameterstore/parameterstore.go

@@ -90,6 +90,6 @@ func (pm *ParameterStore) GetSecretMap(ctx context.Context, ref esv1alpha1.Exter
 	return secretData, nil
 }
 
-func (pm *ParameterStore) Close() error {
+func (pm *ParameterStore) Close(ctx context.Context) error {
 	return nil
 }

+ 27 - 3
pkg/provider/aws/secretsmanager/fake/fake.go

@@ -11,6 +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 fake
 
 import (
@@ -22,15 +23,38 @@ import (
 
 // Client implements the aws secretsmanager interface.
 type Client struct {
-	valFn func(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error)
+	ExecutionCounter int
+	valFn            map[string]func(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error)
+}
+
+// NewClient init a new fake client.
+func NewClient() *Client {
+	return &Client{
+		valFn: make(map[string]func(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error)),
+	}
 }
 
 func (sm *Client) GetSecretValue(in *awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error) {
-	return sm.valFn(in)
+	sm.ExecutionCounter++
+	if entry, found := sm.valFn[sm.cacheKeyForInput(in)]; found {
+		return entry(in)
+	}
+	return nil, fmt.Errorf("test case not found")
+}
+
+func (sm *Client) cacheKeyForInput(in *awssm.GetSecretValueInput) string {
+	var secretID, versionID string
+	if in.SecretId != nil {
+		secretID = *in.SecretId
+	}
+	if in.VersionId != nil {
+		versionID = *in.VersionId
+	}
+	return fmt.Sprintf("%s#%s", secretID, versionID)
 }
 
 func (sm *Client) WithValue(in *awssm.GetSecretValueInput, val *awssm.GetSecretValueOutput, err error) {
-	sm.valFn = func(paramIn *awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error) {
+	sm.valFn[sm.cacheKeyForInput(in)] = func(paramIn *awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error) {
 		if !cmp.Equal(paramIn, in) {
 			return nil, fmt.Errorf("unexpected test argument")
 		}

+ 23 - 3
pkg/provider/aws/secretsmanager/secretsmanager.go

@@ -11,6 +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 secretsmanager
 
 import (
@@ -30,6 +31,7 @@ import (
 // SecretsManager is a provider for AWS SecretsManager.
 type SecretsManager struct {
 	client SMInterface
+	cache  map[string]*awssm.GetSecretValueOutput
 }
 
 // SMInterface is a subset of the smiface api.
@@ -44,20 +46,37 @@ var log = ctrl.Log.WithName("provider").WithName("aws").WithName("secretsmanager
 func New(sess client.ConfigProvider) (*SecretsManager, error) {
 	return &SecretsManager{
 		client: awssm.New(sess),
+		cache:  make(map[string]*awssm.GetSecretValueOutput),
 	}, nil
 }
 
-// GetSecret returns a single secret from the provider.
-func (sm *SecretsManager) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+func (sm *SecretsManager) fetch(_ context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (*awssm.GetSecretValueOutput, error) {
 	ver := "AWSCURRENT"
 	if ref.Version != "" {
 		ver = ref.Version
 	}
 	log.Info("fetching secret value", "key", ref.Key, "version", ver)
+
+	cacheKey := fmt.Sprintf("%s#%s", ref.Key, ver)
+	if secretOut, found := sm.cache[cacheKey]; found {
+		log.Info("found secret in cache", "key", ref.Key, "version", ver)
+		return secretOut, nil
+	}
 	secretOut, err := sm.client.GetSecretValue(&awssm.GetSecretValueInput{
 		SecretId:     &ref.Key,
 		VersionStage: &ver,
 	})
+	if err != nil {
+		return nil, err
+	}
+	sm.cache[cacheKey] = secretOut
+
+	return secretOut, nil
+}
+
+// GetSecret returns a single secret from the provider.
+func (sm *SecretsManager) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	secretOut, err := sm.fetch(ctx, ref)
 	if err != nil {
 		return nil, util.SanitizeErr(err)
 	}
@@ -77,6 +96,7 @@ func (sm *SecretsManager) GetSecret(ctx context.Context, ref esv1alpha1.External
 	if secretOut.SecretBinary != nil {
 		payload = string(secretOut.SecretBinary)
 	}
+
 	val := gjson.Get(payload, ref.Property)
 	if !val.Exists() {
 		return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
@@ -103,6 +123,6 @@ func (sm *SecretsManager) GetSecretMap(ctx context.Context, ref esv1alpha1.Exter
 	return secretData, nil
 }
 
-func (sm *SecretsManager) Close() error {
+func (sm *SecretsManager) Close(ctx context.Context) error {
 	return nil
 }

+ 75 - 4
pkg/provider/aws/secretsmanager/secretsmanager_test.go

@@ -11,6 +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 secretsmanager
 
 import (
@@ -37,11 +38,13 @@ type secretsManagerTestCase struct {
 	expectedSecret string
 	// for testing secretmap
 	expectedData map[string][]byte
+	// for testing caching
+	expectedCounter *int
 }
 
 func makeValidSecretsManagerTestCase() *secretsManagerTestCase {
 	smtc := secretsManagerTestCase{
-		fakeClient:     &fakesm.Client{},
+		fakeClient:     fakesm.NewClient(),
 		apiInput:       makeValidAPIInput(),
 		remoteRef:      makeValidRemoteRef(),
 		apiOutput:      makeValidAPIOutput(),
@@ -164,8 +167,59 @@ func TestSecretsManagerGetSecret(t *testing.T) {
 		makeValidSecretsManagerTestCaseCustom(setAPIErr),
 	}
 
-	sm := SecretsManager{}
 	for k, v := range successCases {
+		sm := SecretsManager{
+			cache:  make(map[string]*awssm.GetSecretValueOutput),
+			client: v.fakeClient,
+		}
+		out, err := sm.GetSecret(context.Background(), *v.remoteRef)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
+		}
+		if err == nil && string(out) != v.expectedSecret {
+			t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out))
+		}
+	}
+}
+func TestCaching(t *testing.T) {
+	fakeClient := fakesm.NewClient()
+
+	// good case: first call, since we are using the same key, results should be cached and the counter should not go
+	// over 1
+	firstCall := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = aws.String(`{"foo":"bar", "bar":"vodka"}`)
+		smtc.remoteRef.Property = "foo"
+		smtc.expectedSecret = "bar"
+		smtc.expectedCounter = aws.Int(1)
+		smtc.fakeClient = fakeClient
+	}
+	secondCall := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = aws.String(`{"foo":"bar", "bar":"vodka"}`)
+		smtc.remoteRef.Property = "bar"
+		smtc.expectedSecret = "vodka"
+		smtc.expectedCounter = aws.Int(1)
+		smtc.fakeClient = fakeClient
+	}
+	notCachedCall := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = aws.String(`{"sheldon":"bazinga", "bar":"foo"}`)
+		smtc.remoteRef.Property = "sheldon"
+		smtc.expectedSecret = "bazinga"
+		smtc.expectedCounter = aws.Int(2)
+		smtc.fakeClient = fakeClient
+		smtc.apiInput.SecretId = aws.String("xyz")
+		smtc.remoteRef.Key = "xyz" // it should reset the cache since the key is different
+	}
+
+	cachedCases := []*secretsManagerTestCase{
+		makeValidSecretsManagerTestCaseCustom(firstCall),
+		makeValidSecretsManagerTestCaseCustom(firstCall),
+		makeValidSecretsManagerTestCaseCustom(secondCall),
+		makeValidSecretsManagerTestCaseCustom(notCachedCall),
+	}
+	sm := SecretsManager{
+		cache: make(map[string]*awssm.GetSecretValueOutput),
+	}
+	for k, v := range cachedCases {
 		sm.client = v.fakeClient
 		out, err := sm.GetSecret(context.Background(), *v.remoteRef)
 		if !ErrorContains(err, v.expectError) {
@@ -174,6 +228,9 @@ func TestSecretsManagerGetSecret(t *testing.T) {
 		if err == nil && string(out) != v.expectedSecret {
 			t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out))
 		}
+		if v.expectedCounter != nil && v.fakeClient.ExecutionCounter != *v.expectedCounter {
+			t.Errorf("[%d] unexpected counter value: expected %d, got %d", k, v.expectedCounter, v.fakeClient.ExecutionCounter)
+		}
 	}
 }
 
@@ -184,6 +241,14 @@ func TestGetSecretMap(t *testing.T) {
 		smtc.expectedData["foo"] = []byte("bar")
 	}
 
+	// good case: caching
+	cachedMap := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = aws.String(`{"foo":"bar", "plus": "one"}`)
+		smtc.expectedData["foo"] = []byte("bar")
+		smtc.expectedData["plus"] = []byte("one")
+		smtc.expectedCounter = aws.Int(1)
+	}
+
 	// bad case: invalid json
 	setInvalidJSON := func(smtc *secretsManagerTestCase) {
 		smtc.apiOutput.SecretString = aws.String(`-----------------`)
@@ -194,11 +259,14 @@ func TestGetSecretMap(t *testing.T) {
 		makeValidSecretsManagerTestCaseCustom(setDeserialization),
 		makeValidSecretsManagerTestCaseCustom(setAPIErr),
 		makeValidSecretsManagerTestCaseCustom(setInvalidJSON),
+		makeValidSecretsManagerTestCaseCustom(cachedMap),
 	}
 
-	sm := SecretsManager{}
 	for k, v := range successCases {
-		sm.client = v.fakeClient
+		sm := SecretsManager{
+			cache:  make(map[string]*awssm.GetSecretValueOutput),
+			client: v.fakeClient,
+		}
 		out, err := sm.GetSecretMap(context.Background(), *v.remoteRef)
 		if !ErrorContains(err, v.expectError) {
 			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
@@ -206,6 +274,9 @@ func TestGetSecretMap(t *testing.T) {
 		if err == nil && !cmp.Equal(out, v.expectedData) {
 			t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedData, out)
 		}
+		if v.expectedCounter != nil && v.fakeClient.ExecutionCounter != *v.expectedCounter {
+			t.Errorf("[%d] unexpected counter value: expected %d, got %d", k, v.expectedCounter, v.fakeClient.ExecutionCounter)
+		}
 	}
 }
 

+ 1 - 1
pkg/provider/azure/keyvault/keyvault.go

@@ -227,7 +227,7 @@ func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef sm
 	return value, nil
 }
 
-func (a *Azure) Close() error {
+func (a *Azure) Close(ctx context.Context) error {
 	return nil
 }
 

+ 1 - 1
pkg/provider/fake/fake.go

@@ -74,7 +74,7 @@ func (v *Client) WithGetSecret(secData []byte, err error) *Client {
 func (v *Client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 	return v.GetSecretMapFn(ctx, ref)
 }
-func (v *Client) Close() error {
+func (v *Client) Close(ctx context.Context) error {
 	return nil
 }
 

+ 24 - 12
pkg/provider/gcp/secretmanager/secretsmanager.go

@@ -39,12 +39,12 @@ const (
 	defaultVersion    = "latest"
 
 	errGCPSMStore                             = "received invalid GCPSM SecretStore resource"
-	errGCPSMCredSecretName                    = "invalid GCPSM SecretStore resource: missing GCP Secret Access Key"
 	errClientClose                            = "unable to close SecretManager client: %w"
 	errInvalidClusterStoreMissingSAKNamespace = "invalid ClusterSecretStore: missing GCP SecretAccessKey Namespace"
 	errFetchSAKSecret                         = "could not fetch SecretAccessKey secret: %w"
 	errMissingSAK                             = "missing SecretAccessKey"
 	errUnableProcessJSONCredentials           = "failed to process the provided JSON credentials: %w"
+	errUnableProcessDefaultCredentials        = "failed to process the default credentials: %w"
 	errUnableCreateGCPSMClient                = "failed to create GCP secretmanager client: %w"
 	errUninitalizedGCPProvider                = "provider GCP is not initialized"
 	errClientGetSecretAccess                  = "unable to access Secret from SecretManager Client: %w"
@@ -73,9 +73,6 @@ type gClient struct {
 func (c *gClient) setAuth(ctx context.Context) error {
 	credentialsSecret := &corev1.Secret{}
 	credentialsSecretName := c.store.Auth.SecretRef.SecretAccessKey.Name
-	if credentialsSecretName == "" {
-		return fmt.Errorf(errGCPSMCredSecretName)
-	}
 	objectKey := types.NamespacedName{
 		Name:      credentialsSecretName,
 		Namespace: c.namespace,
@@ -83,12 +80,16 @@ func (c *gClient) setAuth(ctx context.Context) error {
 
 	// only ClusterStore is allowed to set namespace (and then it's required)
 	if c.storeKind == esv1alpha1.ClusterSecretStoreKind {
-		if c.store.Auth.SecretRef.SecretAccessKey.Namespace == nil {
+		if credentialsSecretName != "" && c.store.Auth.SecretRef.SecretAccessKey.Namespace == nil {
 			return fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
+		} else if credentialsSecretName != "" {
+			objectKey.Namespace = *c.store.Auth.SecretRef.SecretAccessKey.Namespace
 		}
-		objectKey.Namespace = *c.store.Auth.SecretRef.SecretAccessKey.Namespace
 	}
-
+	if credentialsSecretName == "" {
+		c.credentials = nil
+		return nil
+	}
 	err := c.kube.Get(ctx, objectKey, credentialsSecret)
 	if err != nil {
 		return fmt.Errorf(errFetchSAKSecret, err)
@@ -122,12 +123,23 @@ func (sm *ProviderGCP) NewClient(ctx context.Context, store esv1alpha1.GenericSt
 
 	sm.projectID = cliStore.store.ProjectID
 
-	config, err := google.JWTConfigFromJSON(cliStore.credentials, CloudPlatformRole)
+	if cliStore.credentials != nil {
+		config, err := google.JWTConfigFromJSON(cliStore.credentials, CloudPlatformRole)
+		if err != nil {
+			return nil, fmt.Errorf(errUnableProcessJSONCredentials, err)
+		}
+		ts := config.TokenSource(ctx)
+		clientGCPSM, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
+		if err != nil {
+			return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
+		}
+		sm.SecretManagerClient = clientGCPSM
+		return sm, nil
+	}
+	ts, err := google.DefaultTokenSource(ctx, CloudPlatformRole)
 	if err != nil {
-		return nil, fmt.Errorf(errUnableProcessJSONCredentials, err)
+		return nil, fmt.Errorf(errUnableProcessDefaultCredentials, err)
 	}
-	ts := config.TokenSource(ctx)
-
 	clientGCPSM, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
 	if err != nil {
 		return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
@@ -199,7 +211,7 @@ func (sm *ProviderGCP) GetSecretMap(ctx context.Context, ref esv1alpha1.External
 	return secretData, nil
 }
 
-func (sm *ProviderGCP) Close() error {
+func (sm *ProviderGCP) Close(ctx context.Context) error {
 	err := sm.SecretManagerClient.Close()
 	if err != nil {
 		return fmt.Errorf(errClientClose, err)

+ 39 - 0
pkg/provider/gitlab/fake/fake.go

@@ -0,0 +1,39 @@
+/*
+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 fake
+
+import (
+	gitlab "github.com/xanzy/go-gitlab"
+)
+
+type GitlabMockClient struct {
+	getVariable func(pid interface{}, key string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectVariable, *gitlab.Response, error)
+}
+
+func (mc *GitlabMockClient) GetVariable(pid interface{}, key string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectVariable, *gitlab.Response, error) {
+	return mc.getVariable(pid, key, nil)
+}
+
+func (mc *GitlabMockClient) WithValue(projectIDinput, keyInput string, output *gitlab.ProjectVariable, err error) {
+	if mc != nil {
+		mc.getVariable = func(pid interface{}, key string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectVariable, *gitlab.Response, error) {
+			// type secretmanagerpb.AccessSecretVersionRequest contains unexported fields
+			// use cmpopts.IgnoreUnexported to ignore all the unexported fields in the cmp.
+			// if !cmp.Equal(paramReq, input, cmpopts.IgnoreUnexported(gitlab.ProjectVariable{})) {
+			// 	return nil, nil, fmt.Errorf("unexpected test argument")
+			// }
+			return output, nil, err
+		}
+	}
+}

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

@@ -0,0 +1,212 @@
+/*
+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 gitlab
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/tidwall/gjson"
+	gitlab "github.com/xanzy/go-gitlab"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"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"
+)
+
+// Requires GITLAB_TOKEN and GITLAB_PROJECT_ID to be set in environment variables
+
+const (
+	errGitlabCredSecretName                   = "credentials are empty"
+	errInvalidClusterStoreMissingSAKNamespace = "invalid clusterStore missing SAK namespace"
+	errFetchSAKSecret                         = "couldn't find secret on cluster: %w"
+	errMissingSAK                             = "missing credentials while setting auth"
+	errUninitalizedGitlabProvider             = "provider gitlab is not initialized"
+	errJSONSecretUnmarshal                    = "unable to unmarshal secret: %w"
+)
+
+type Client interface {
+	GetVariable(pid interface{}, key string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectVariable, *gitlab.Response, error)
+}
+
+// Gitlab Provider struct with reference to a GitLab client and a projectID.
+type Gitlab struct {
+	client    Client
+	projectID interface{}
+}
+
+// Client for interacting with kubernetes cluster...?
+type gClient struct {
+	kube        kclient.Client
+	store       *esv1alpha1.GitlabProvider
+	namespace   string
+	storeKind   string
+	credentials []byte
+}
+
+func init() {
+	schema.Register(&Gitlab{}, &esv1alpha1.SecretStoreProvider{
+		Gitlab: &esv1alpha1.GitlabProvider{},
+	})
+}
+
+// Set gClient credentials to Access Token.
+func (c *gClient) setAuth(ctx context.Context) error {
+	credentialsSecret := &corev1.Secret{}
+	credentialsSecretName := c.store.Auth.SecretRef.AccessToken.Name
+	if credentialsSecretName == "" {
+		return fmt.Errorf(errGitlabCredSecretName)
+	}
+	objectKey := types.NamespacedName{
+		Name:      credentialsSecretName,
+		Namespace: c.namespace,
+	}
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if c.storeKind == esv1alpha1.ClusterSecretStoreKind {
+		if c.store.Auth.SecretRef.AccessToken.Namespace == nil {
+			return fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
+		}
+		objectKey.Namespace = *c.store.Auth.SecretRef.AccessToken.Namespace
+	}
+
+	err := c.kube.Get(ctx, objectKey, credentialsSecret)
+	if err != nil {
+		return fmt.Errorf(errFetchSAKSecret, err)
+	}
+
+	c.credentials = credentialsSecret.Data[c.store.Auth.SecretRef.AccessToken.Key]
+	if (c.credentials == nil) || (len(c.credentials) == 0) {
+		return fmt.Errorf(errMissingSAK)
+	}
+	// I don't know where ProjectID is being set
+	// This line SHOULD set it, but instead just breaks everything :)
+	// c.store.ProjectID = string(credentialsSecret.Data[c.store.ProjectID])
+	return nil
+}
+
+// Function newGitlabProvider returns a reference to a new instance of a 'Gitlab' struct.
+func NewGitlabProvider() *Gitlab {
+	return &Gitlab{}
+}
+
+// Method on Gitlab Provider to set up client with credentials and populate projectID.
+func (g *Gitlab) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.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")
+	}
+	storeSpecGitlab := storeSpec.Provider.Gitlab
+
+	cliStore := gClient{
+		kube:      kube,
+		store:     storeSpecGitlab,
+		namespace: namespace,
+		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
+	}
+
+	if err := cliStore.setAuth(ctx); err != nil {
+		return nil, err
+	}
+
+	var err error
+
+	// Create client options
+	var opts []gitlab.ClientOptionFunc
+	if cliStore.store.URL != "" {
+		opts = append(opts, gitlab.WithBaseURL(cliStore.store.URL))
+	}
+	// ClientOptionFunc from the gitlab package can be mapped with the CRD
+	// in a similar way to extend functionality of the provider
+
+	// Create a new Gitlab client using credentials and options
+	gitlabClient, err := gitlab.NewClient(string(cliStore.credentials), opts...)
+	if err != nil {
+		log.Logf("Failed to create client: %v", err)
+	}
+
+	g.client = gitlabClient.ProjectVariables
+	g.projectID = cliStore.store.ProjectID
+
+	return g, nil
+}
+
+func (g *Gitlab) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	if utils.IsNil(g.client) {
+		return nil, fmt.Errorf(errUninitalizedGitlabProvider)
+	}
+	// Need to replace hyphens with underscores to work with Gitlab API
+	ref.Key = strings.ReplaceAll(ref.Key, "-", "_")
+	// Retrieves a gitlab variable in the form
+	// {
+	// 	"key": "TEST_VARIABLE_1",
+	// 	"variable_type": "env_var",
+	// 	"value": "TEST_1",
+	// 	"protected": false,
+	// 	"masked": true
+	data, _, err := g.client.GetVariable(g.projectID, ref.Key, nil) // Optional 'filter' parameter could be added later
+	if err != nil {
+		return nil, err
+	}
+
+	if ref.Property == "" {
+		if data.Value != "" {
+			return []byte(data.Value), nil
+		}
+		return nil, fmt.Errorf("invalid secret received. no secret string for key: %s", ref.Key)
+	}
+
+	var payload string
+	if data.Value != "" {
+		payload = data.Value
+	}
+
+	val := gjson.Get(payload, ref.Property)
+	if !val.Exists() {
+		return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
+	}
+	return []byte(val.String()), nil
+}
+
+func (g *Gitlab) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	// Gets a secret as normal, expecting secret value to be a json object
+	data, err := g.GetSecret(ctx, ref)
+	if err != nil {
+		return nil, fmt.Errorf("error getting secret %s: %w", ref.Key, err)
+	}
+
+	// Maps the json data to a string:string map
+	kv := make(map[string]string)
+	err = json.Unmarshal(data, &kv)
+	if err != nil {
+		return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
+	}
+
+	// Converts values in K:V pairs into bytes, while leaving keys as strings
+	secretData := make(map[string][]byte)
+	for k, v := range kv {
+		secretData[k] = []byte(v)
+	}
+	return secretData, nil
+}
+
+func (g *Gitlab) Close(ctx context.Context) error {
+	return nil
+}

+ 178 - 0
pkg/provider/gitlab/gitlab_test.go

@@ -0,0 +1,178 @@
+/*
+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 gitlab
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"strings"
+	"testing"
+
+	gitlab "github.com/xanzy/go-gitlab"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	fakegitlab "github.com/external-secrets/external-secrets/pkg/provider/gitlab/fake"
+)
+
+type secretManagerTestCase struct {
+	mockClient        *fakegitlab.GitlabMockClient
+	apiInputProjectID string
+	apiInputKey       string
+	apiOutput         *gitlab.ProjectVariable
+	ref               *esv1alpha1.ExternalSecretDataRemoteRef
+	projectID         *string
+	apiErr            error
+	expectError       string
+	expectedSecret    string
+	// for testing secretmap
+	expectedData map[string][]byte
+}
+
+func makeValidSecretManagerTestCase() *secretManagerTestCase {
+	smtc := secretManagerTestCase{
+		mockClient:        &fakegitlab.GitlabMockClient{},
+		apiInputProjectID: makeValidAPIInputProjectID(),
+		apiInputKey:       makeValidAPIInputKey(),
+		ref:               makeValidRef(),
+		projectID:         nil,
+		apiOutput:         makeValidAPIOutput(),
+		apiErr:            nil,
+		expectError:       "",
+		expectedSecret:    "",
+		expectedData:      map[string][]byte{},
+	}
+	smtc.mockClient.WithValue(smtc.apiInputProjectID, smtc.apiInputKey, smtc.apiOutput, smtc.apiErr)
+	return &smtc
+}
+
+func makeValidRef() *esv1alpha1.ExternalSecretDataRemoteRef {
+	return &esv1alpha1.ExternalSecretDataRemoteRef{
+		Key:     "test-secret",
+		Version: "default",
+	}
+}
+
+func makeValidAPIInputProjectID() string {
+	return "testID"
+}
+
+func makeValidAPIInputKey() string {
+	return "testKey"
+}
+
+func makeValidAPIOutput() *gitlab.ProjectVariable {
+	return &gitlab.ProjectVariable{
+		Key:   "testKey",
+		Value: "",
+	}
+}
+
+func makeValidSecretManagerTestCaseCustom(tweaks ...func(smtc *secretManagerTestCase)) *secretManagerTestCase {
+	smtc := makeValidSecretManagerTestCase()
+	for _, fn := range tweaks {
+		fn(smtc)
+	}
+	smtc.mockClient.WithValue(smtc.apiInputProjectID, smtc.apiInputKey, smtc.apiOutput, smtc.apiErr)
+	return smtc
+}
+
+// This case can be shared by both GetSecret and GetSecretMap tests.
+// bad case: set apiErr.
+var setAPIErr = func(smtc *secretManagerTestCase) {
+	smtc.apiErr = fmt.Errorf("oh no")
+	smtc.expectError = "oh no"
+}
+
+var setNilMockClient = func(smtc *secretManagerTestCase) {
+	smtc.mockClient = nil
+	smtc.expectError = errUninitalizedGitlabProvider
+}
+
+// test the sm<->gcp interface
+// make sure correct values are passed and errors are handled accordingly.
+func TestGitlabSecretManagerGetSecret(t *testing.T) {
+	secretValue := "changedvalue"
+	// good case: default version is set
+	// key is passed in, output is sent back
+
+	setSecretString := func(smtc *secretManagerTestCase) {
+		smtc.apiOutput = &gitlab.ProjectVariable{
+			Key:   "testkey",
+			Value: "changedvalue",
+		}
+		smtc.expectedSecret = secretValue
+	}
+
+	successCases := []*secretManagerTestCase{
+		makeValidSecretManagerTestCaseCustom(setSecretString),
+		makeValidSecretManagerTestCaseCustom(setAPIErr),
+		makeValidSecretManagerTestCaseCustom(setNilMockClient),
+	}
+
+	sm := Gitlab{}
+	for k, v := range successCases {
+		sm.client = v.mockClient
+		out, err := sm.GetSecret(context.Background(), *v.ref)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
+		}
+		if string(out) != v.expectedSecret {
+			t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out))
+		}
+	}
+}
+
+func TestGetSecretMap(t *testing.T) {
+	// good case: default version & deserialization
+	setDeserialization := func(smtc *secretManagerTestCase) {
+		smtc.apiOutput.Value = `{"foo":"bar"}`
+		smtc.expectedData["foo"] = []byte("bar")
+	}
+
+	// bad case: invalid json
+	setInvalidJSON := func(smtc *secretManagerTestCase) {
+		smtc.apiOutput.Value = `-----------------`
+		smtc.expectError = "unable to unmarshal secret"
+	}
+
+	successCases := []*secretManagerTestCase{
+		makeValidSecretManagerTestCaseCustom(setDeserialization),
+		makeValidSecretManagerTestCaseCustom(setInvalidJSON),
+		makeValidSecretManagerTestCaseCustom(setNilMockClient),
+		makeValidSecretManagerTestCaseCustom(setAPIErr),
+	}
+
+	sm := Gitlab{}
+	for k, v := range successCases {
+		sm.client = v.mockClient
+		out, err := sm.GetSecretMap(context.Background(), *v.ref)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
+		}
+		if err == nil && !reflect.DeepEqual(out, v.expectedData) {
+			t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedData, out)
+		}
+	}
+}
+
+func ErrorContains(out error, want string) bool {
+	if out == nil {
+		return want == ""
+	}
+	if want == "" {
+		return false
+	}
+	return strings.Contains(out.Error(), want)
+}

+ 1 - 1
pkg/provider/ibm/provider.go

@@ -289,7 +289,7 @@ func (ibm *providerIBM) GetSecretMap(ctx context.Context, ref esv1alpha1.Externa
 	}
 }
 
-func (ibm *providerIBM) Close() error {
+func (ibm *providerIBM) Close(ctx context.Context) error {
 	return nil
 }
 

+ 36 - 0
pkg/provider/oracle/fake/fake.go

@@ -0,0 +1,36 @@
+/*
+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 fake
+
+import (
+	"context"
+
+	vault "github.com/oracle/oci-go-sdk/v45/vault"
+)
+
+type OracleMockClient struct {
+	getSecret func(ctx context.Context, request vault.GetSecretRequest) (response vault.GetSecretResponse, err error)
+}
+
+func (mc *OracleMockClient) GetSecret(ctx context.Context, request vault.GetSecretRequest) (response vault.GetSecretResponse, err error) {
+	return mc.getSecret(ctx, request)
+}
+
+func (mc *OracleMockClient) WithValue(input vault.GetSecretRequest, output vault.GetSecretResponse, err error) {
+	if mc != nil {
+		mc.getSecret = func(ctx context.Context, paramReq vault.GetSecretRequest) (vault.GetSecretResponse, error) {
+			return output, err
+		}
+	}
+}

+ 213 - 0
pkg/provider/oracle/oracle.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 oracle
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/oracle/oci-go-sdk/v45/common"
+	vault "github.com/oracle/oci-go-sdk/v45/vault"
+	"github.com/tidwall/gjson"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"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"
+)
+
+const (
+	VaultEndpointEnv = "ORACLE_VAULT_ENDPOINT"
+	STSEndpointEnv   = "ORACLE_STS_ENDPOINT"
+	SVMEndpointEnv   = "ORACLE_SVM_ENDPOINT"
+
+	errOracleClient                          = "cannot setup new oracle client: %w"
+	errORACLECredSecretName                  = "invalid oracle SecretStore resource: missing oracle APIKey"
+	errUninitalizedOracleProvider            = "provider oracle is not initialized"
+	errInvalidClusterStoreMissingSKNamespace = "invalid ClusterStore, missing namespace"
+	errFetchSAKSecret                        = "could not fetch SecretAccessKey secret: %w"
+	errMissingPK                             = "missing PrivateKey"
+	errMissingUser                           = "missing User ID"
+	errMissingTenancy                        = "missing Tenancy ID"
+	errMissingRegion                         = "missing Region"
+	errMissingFingerprint                    = "missing Fingerprint"
+	errJSONSecretUnmarshal                   = "unable to unmarshal secret: %w"
+	errMissingKey                            = "missing Key in secret: %s"
+	errInvalidSecret                         = "invalid secret received. no secret string nor binary for key: %s"
+)
+
+type client struct {
+	kube        kclient.Client
+	store       *esv1alpha1.OracleProvider
+	namespace   string
+	storeKind   string
+	tenancy     string
+	user        string
+	region      string
+	fingerprint string
+	privateKey  string
+}
+
+type VaultManagementService struct {
+	Client VMInterface
+}
+
+type VMInterface interface {
+	GetSecret(ctx context.Context, request vault.GetSecretRequest) (response vault.GetSecretResponse, err error)
+}
+
+func (c *client) setAuth(ctx context.Context) error {
+	credentialsSecret := &corev1.Secret{}
+	credentialsSecretName := c.store.Auth.SecretRef.PrivateKey.Name
+	if credentialsSecretName == "" {
+		return fmt.Errorf(errORACLECredSecretName)
+	}
+	objectKey := types.NamespacedName{
+		Name:      credentialsSecretName,
+		Namespace: c.namespace,
+	}
+
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if c.storeKind == esv1alpha1.ClusterSecretStoreKind {
+		if c.store.Auth.SecretRef.PrivateKey.Namespace == nil {
+			return fmt.Errorf(errInvalidClusterStoreMissingSKNamespace)
+		}
+		objectKey.Namespace = *c.store.Auth.SecretRef.PrivateKey.Namespace
+	}
+
+	err := c.kube.Get(ctx, objectKey, credentialsSecret)
+	if err != nil {
+		return fmt.Errorf(errFetchSAKSecret, err)
+	}
+
+	c.privateKey = string(credentialsSecret.Data[c.store.Auth.SecretRef.PrivateKey.Key])
+	if c.privateKey == "" {
+		return fmt.Errorf(errMissingPK)
+	}
+
+	c.fingerprint = string(credentialsSecret.Data[c.store.Auth.SecretRef.Fingerprint.Key])
+	if c.fingerprint == "" {
+		return fmt.Errorf(errMissingFingerprint)
+	}
+
+	c.user = c.store.User
+	if c.user == "" {
+		return fmt.Errorf(errMissingUser)
+	}
+
+	c.tenancy = c.store.Tenancy
+	if c.tenancy == "" {
+		return fmt.Errorf(errMissingTenancy)
+	}
+
+	c.region = c.store.Region
+	if c.region == "" {
+		return fmt.Errorf(errMissingRegion)
+	}
+
+	return nil
+}
+
+func (vms *VaultManagementService) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	if utils.IsNil(vms.Client) {
+		return nil, fmt.Errorf(errUninitalizedOracleProvider)
+	}
+	vmsRequest := vault.GetSecretRequest{
+		SecretId: &ref.Key,
+	}
+	secretOut, err := vms.Client.GetSecret(context.Background(), vmsRequest)
+	if err != nil {
+		return nil, util.SanitizeErr(err)
+	}
+	if ref.Property == "" {
+		if *secretOut.SecretName != "" {
+			return []byte(*secretOut.SecretName), nil
+		}
+		return nil, fmt.Errorf(errInvalidSecret, ref.Key)
+	}
+	var payload *string
+	if secretOut.SecretName != nil {
+		payload = secretOut.SecretName
+	}
+
+	payloadval := *payload
+
+	val := gjson.Get(payloadval, ref.Property)
+	if !val.Exists() {
+		return nil, fmt.Errorf(errMissingKey, ref.Key)
+	}
+
+	return []byte(val.String()), nil
+}
+
+func (vms *VaultManagementService) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	data, err := vms.GetSecret(ctx, ref)
+	if err != nil {
+		return nil, err
+	}
+	kv := make(map[string]string)
+	err = json.Unmarshal(data, &kv)
+	if err != nil {
+		return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
+	}
+	secretData := make(map[string][]byte)
+	for k, v := range kv {
+		secretData[k] = []byte(v)
+	}
+	return secretData, nil
+}
+
+// NewClient constructs a new secrets client based on the provided store.
+func (vms *VaultManagementService) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
+	storeSpec := store.GetSpec()
+	oracleSpec := storeSpec.Provider.Oracle
+
+	oracleStore := &client{
+		kube:      kube,
+		store:     oracleSpec,
+		namespace: namespace,
+		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
+	}
+	if err := oracleStore.setAuth(ctx); err != nil {
+		return nil, err
+	}
+
+	oracleTenancy := oracleStore.tenancy
+	oracleUser := oracleStore.user
+	oracleRegion := oracleStore.region
+	oracleFingerprint := oracleStore.fingerprint
+	oraclePrivateKey := oracleStore.privateKey
+
+	configurationProvider := common.NewRawConfigurationProvider(oracleTenancy, oracleUser, oracleRegion, oracleFingerprint, oraclePrivateKey, nil)
+
+	vaultManagementService, err := vault.NewVaultsClientWithConfigurationProvider(configurationProvider)
+	if err != nil {
+		return nil, fmt.Errorf(errOracleClient, err)
+	}
+	vms.Client = vaultManagementService
+	return vms, nil
+}
+
+func (vms *VaultManagementService) Close(ctx context.Context) error {
+	return nil
+}
+
+func init() {
+	schema.Register(&VaultManagementService{}, &esv1alpha1.SecretStoreProvider{
+		Oracle: &esv1alpha1.OracleProvider{},
+	})
+}

+ 173 - 0
pkg/provider/oracle/oracle_test.go

@@ -0,0 +1,173 @@
+/*
+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 oracle
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"strings"
+	"testing"
+
+	vault "github.com/oracle/oci-go-sdk/v45/vault"
+	utilpointer "k8s.io/utils/pointer"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	fakeoracle "github.com/external-secrets/external-secrets/pkg/provider/oracle/fake"
+)
+
+type vaultTestCase struct {
+	mockClient     *fakeoracle.OracleMockClient
+	apiInput       *vault.GetSecretRequest
+	apiOutput      *vault.GetSecretResponse
+	ref            *esv1alpha1.ExternalSecretDataRemoteRef
+	apiErr         error
+	expectError    string
+	expectedSecret string
+	// for testing secretmap
+	expectedData map[string][]byte
+}
+
+func makeValidVaultTestCase() *vaultTestCase {
+	smtc := vaultTestCase{
+		mockClient:     &fakeoracle.OracleMockClient{},
+		apiInput:       makeValidAPIInput(),
+		ref:            makeValidRef(),
+		apiOutput:      makeValidAPIOutput(),
+		apiErr:         nil,
+		expectError:    "",
+		expectedSecret: "",
+		expectedData:   map[string][]byte{},
+	}
+	smtc.mockClient.WithValue(*smtc.apiInput, *smtc.apiOutput, smtc.apiErr)
+	return &smtc
+}
+
+func makeValidRef() *esv1alpha1.ExternalSecretDataRemoteRef {
+	return &esv1alpha1.ExternalSecretDataRemoteRef{
+		Key:     "test-secret",
+		Version: "default",
+	}
+}
+
+func makeValidAPIInput() *vault.GetSecretRequest {
+	return &vault.GetSecretRequest{
+		SecretId: utilpointer.StringPtr("test-secret"),
+	}
+}
+
+func makeValidAPIOutput() *vault.GetSecretResponse {
+	return &vault.GetSecretResponse{
+		Etag:   utilpointer.StringPtr("test-name"),
+		Secret: vault.Secret{},
+	}
+}
+
+func makeValidVaultTestCaseCustom(tweaks ...func(smtc *vaultTestCase)) *vaultTestCase {
+	smtc := makeValidVaultTestCase()
+	for _, fn := range tweaks {
+		fn(smtc)
+	}
+	smtc.mockClient.WithValue(*smtc.apiInput, *smtc.apiOutput, smtc.apiErr)
+	return smtc
+}
+
+// This case can be shared by both GetSecret and GetSecretMap tests.
+// bad case: set apiErr.
+var setAPIErr = func(smtc *vaultTestCase) {
+	smtc.apiErr = fmt.Errorf("oh no")
+	smtc.expectError = "oh no"
+}
+
+var setNilMockClient = func(smtc *vaultTestCase) {
+	smtc.mockClient = nil
+	smtc.expectError = errUninitalizedOracleProvider
+}
+
+func TestOracleVaultGetSecret(t *testing.T) {
+	secretValue := "changedvalue"
+	// good case: default version is set
+	// key is passed in, output is sent back
+	setSecretString := func(smtc *vaultTestCase) {
+		smtc.apiOutput = &vault.GetSecretResponse{
+			Etag: utilpointer.StringPtr("test-name"),
+			Secret: vault.Secret{
+				CompartmentId: utilpointer.StringPtr("test-compartment-id"),
+				Id:            utilpointer.StringPtr("test-id"),
+				SecretName:    utilpointer.StringPtr("changedvalue"),
+			},
+		}
+		smtc.expectedSecret = secretValue
+	}
+
+	successCases := []*vaultTestCase{
+		makeValidVaultTestCaseCustom(setAPIErr),
+		makeValidVaultTestCaseCustom(setNilMockClient),
+		makeValidVaultTestCaseCustom(setSecretString),
+	}
+
+	sm := VaultManagementService{}
+	for k, v := range successCases {
+		sm.Client = v.mockClient
+		fmt.Println(*v.ref)
+		out, err := sm.GetSecret(context.Background(), *v.ref)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
+		}
+		if string(out) != v.expectedSecret {
+			t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out))
+		}
+	}
+}
+
+func TestGetSecretMap(t *testing.T) {
+	// good case: default version & deserialization
+	setDeserialization := func(smtc *vaultTestCase) {
+		smtc.apiOutput.SecretName = utilpointer.StringPtr(`{"foo":"bar"}`)
+		smtc.expectedData["foo"] = []byte("bar")
+	}
+
+	// bad case: invalid json
+	setInvalidJSON := func(smtc *vaultTestCase) {
+		smtc.apiOutput.SecretName = utilpointer.StringPtr(`-----------------`)
+		smtc.expectError = "unable to unmarshal secret"
+	}
+
+	successCases := []*vaultTestCase{
+		makeValidVaultTestCaseCustom(setDeserialization),
+		makeValidVaultTestCaseCustom(setInvalidJSON),
+		makeValidVaultTestCaseCustom(setNilMockClient),
+		makeValidVaultTestCaseCustom(setAPIErr),
+	}
+
+	sm := VaultManagementService{}
+	for k, v := range successCases {
+		sm.Client = v.mockClient
+		out, err := sm.GetSecretMap(context.Background(), *v.ref)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
+		}
+		if err == nil && !reflect.DeepEqual(out, v.expectedData) {
+			t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedData, out)
+		}
+	}
+}
+
+func ErrorContains(out error, want string) bool {
+	if out == nil {
+		return want == ""
+	}
+	if want == "" {
+		return false
+	}
+	return strings.Contains(out.Error(), want)
+}

+ 1 - 1
pkg/provider/provider.go

@@ -35,5 +35,5 @@ type SecretsClient interface {
 
 	// GetSecretMap returns multiple k/v pairs from the provider
 	GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
-	Close() error
+	Close(ctx context.Context) error
 }

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

@@ -17,9 +17,13 @@ package register
 // packages imported here are registered to the controller schema.
 // nolint:golint
 import (
+	_ "github.com/external-secrets/external-secrets/pkg/provider/alibaba"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/aws"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/oracle"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/vault"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox"
 )

+ 1 - 1
pkg/provider/schema/schema.go

@@ -92,7 +92,7 @@ func GetProvider(s esv1alpha1.GenericStore) (provider.Provider, error) {
 // or an error if the provider is not configured.
 func getProviderName(storeSpec *esv1alpha1.SecretStoreProvider) (string, error) {
 	storeBytes, err := json.Marshal(storeSpec)
-	if err != nil {
+	if err != nil || storeBytes == nil {
 		return "", fmt.Errorf("failed to marshal store spec: %w", err)
 	}
 

+ 1 - 1
pkg/provider/schema/schema_test.go

@@ -41,7 +41,7 @@ func (p *PP) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretData
 	return map[string][]byte{}, nil
 }
 
-func (p *PP) Close() error {
+func (p *PP) Close(ctx context.Context) error {
 	return nil
 }
 

+ 1 - 1
pkg/provider/vault/vault.go

@@ -155,7 +155,7 @@ func (v *client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecret
 	return v.readSecret(ctx, ref.Key, ref.Version)
 }
 
-func (v *client) Close() error {
+func (v *client) Close(ctx context.Context) error {
 	return nil
 }
 

+ 39 - 0
pkg/provider/yandex/lockbox/client/client.go

@@ -0,0 +1,39 @@
+/*
+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 client
+
+import (
+	"context"
+	"time"
+
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+)
+
+// Creates Lockbox clients and Yandex.Cloud IAM tokens.
+type YandexCloudCreator interface {
+	CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (LockboxClient, error)
+	CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*IamToken, error)
+	Now() time.Time
+}
+
+type IamToken struct {
+	Token     string
+	ExpiresAt time.Time
+}
+
+// Responsible for accessing Lockbox secrets.
+type LockboxClient interface {
+	GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error)
+}

+ 151 - 0
pkg/provider/yandex/lockbox/client/fake/fake.go

@@ -0,0 +1,151 @@
+/*
+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 fake
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/uuid"
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
+)
+
+// Fake implementation of YandexCloudCreator.
+type YandexCloudCreator struct {
+	Backend *LockboxBackend
+}
+
+func (c *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
+	return &LockboxClient{c.Backend}, nil
+}
+
+func (c *YandexCloudCreator) CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
+	return c.Backend.getToken(authorizedKey)
+}
+
+func (c *YandexCloudCreator) Now() time.Time {
+	return c.Backend.now
+}
+
+// Fake implementation of LockboxClient.
+type LockboxClient struct {
+	fakeLockboxBackend *LockboxBackend
+}
+
+func (c *LockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
+	return c.fakeLockboxBackend.getEntries(iamToken, secretID, versionID)
+}
+
+// Fakes Yandex Lockbox service backend.
+type LockboxBackend struct {
+	secretMap  map[secretKey]secretValue   // secret specific data
+	versionMap map[versionKey]versionValue // version specific data
+	tokenMap   map[tokenKey]tokenValue     // token specific data
+
+	tokenExpirationDuration time.Duration
+	now                     time.Time // fakes the current time
+}
+
+type secretKey struct {
+	secretID string
+}
+
+type secretValue struct {
+	expectedAuthorizedKey *iamkey.Key // authorized key expected to access the secret
+}
+
+type versionKey struct {
+	secretID  string
+	versionID string
+}
+
+type versionValue struct {
+	entries []*lockbox.Payload_Entry
+}
+
+type tokenKey struct {
+	token string
+}
+
+type tokenValue struct {
+	authorizedKey *iamkey.Key
+	expiresAt     time.Time
+}
+
+func NewLockboxBackend(tokenExpirationDuration time.Duration) *LockboxBackend {
+	return &LockboxBackend{
+		secretMap:               make(map[secretKey]secretValue),
+		versionMap:              make(map[versionKey]versionValue),
+		tokenMap:                make(map[tokenKey]tokenValue),
+		tokenExpirationDuration: tokenExpirationDuration,
+		now:                     time.Time{},
+	}
+}
+
+func (lb *LockboxBackend) CreateSecret(authorizedKey *iamkey.Key, entries ...*lockbox.Payload_Entry) (string, string) {
+	secretID := uuid.NewString()
+	versionID := uuid.NewString()
+
+	lb.secretMap[secretKey{secretID}] = secretValue{authorizedKey}
+	lb.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
+	lb.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
+
+	return secretID, versionID
+}
+
+func (lb *LockboxBackend) AddVersion(secretID string, entries ...*lockbox.Payload_Entry) string {
+	versionID := uuid.NewString()
+
+	lb.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
+	lb.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
+
+	return versionID
+}
+
+func (lb *LockboxBackend) AdvanceClock(duration time.Duration) {
+	lb.now = lb.now.Add(duration)
+}
+
+func (lb *LockboxBackend) getToken(authorizedKey *iamkey.Key) (*client.IamToken, error) {
+	token := uuid.NewString()
+	expiresAt := lb.now.Add(lb.tokenExpirationDuration)
+	lb.tokenMap[tokenKey{token}] = tokenValue{authorizedKey, expiresAt}
+	return &client.IamToken{Token: token, ExpiresAt: expiresAt}, nil
+}
+
+func (lb *LockboxBackend) getEntries(iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
+	if _, ok := lb.secretMap[secretKey{secretID}]; !ok {
+		return nil, fmt.Errorf("secret not found")
+	}
+	if _, ok := lb.versionMap[versionKey{secretID, versionID}]; !ok {
+		return nil, fmt.Errorf("version not found")
+	}
+	if _, ok := lb.tokenMap[tokenKey{iamToken}]; !ok {
+		return nil, fmt.Errorf("unauthenticated")
+	}
+
+	if lb.tokenMap[tokenKey{iamToken}].expiresAt.Before(lb.now) {
+		return nil, fmt.Errorf("iam token expired")
+	}
+	if !cmp.Equal(lb.tokenMap[tokenKey{iamToken}].authorizedKey, lb.secretMap[secretKey{secretID}].expectedAuthorizedKey) {
+		return nil, fmt.Errorf("permission denied")
+	}
+
+	return lb.versionMap[versionKey{secretID, versionID}].entries, nil
+}

+ 144 - 0
pkg/provider/yandex/lockbox/client/grpc/grpc.go

@@ -0,0 +1,144 @@
+/*
+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 grpc
+
+import (
+	"context"
+	"crypto/tls"
+	"time"
+
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/endpoint"
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+	ycsdk "github.com/yandex-cloud/go-sdk"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/keepalive"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
+)
+
+// Implementation of YandexCloudCreator.
+type YandexCloudCreator struct {
+}
+
+func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
+	sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey)
+	if err != nil {
+		return nil, err
+	}
+
+	payloadAPIEndpoint, err := sdk.ApiEndpoint().ApiEndpoint().Get(ctx, &endpoint.GetApiEndpointRequest{
+		ApiEndpointId: "lockbox-payload", // the ID from https://api.cloud.yandex.net/endpoints
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	err = closeSDK(ctx, sdk)
+	if err != nil {
+		return nil, err
+	}
+
+	conn, err := grpc.Dial(payloadAPIEndpoint.Address,
+		grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12})),
+		grpc.WithKeepaliveParams(keepalive.ClientParameters{
+			Time:                time.Second * 30,
+			Timeout:             time.Second * 10,
+			PermitWithoutStream: false,
+		}),
+		grpc.WithUserAgent("external-secrets"),
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return &LockboxClient{lockbox.NewPayloadServiceClient(conn)}, nil
+}
+
+func (lb *YandexCloudCreator) CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
+	sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey)
+	if err != nil {
+		return nil, err
+	}
+
+	iamToken, err := sdk.CreateIAMToken(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	err = closeSDK(ctx, sdk)
+	if err != nil {
+		return nil, err
+	}
+
+	return &client.IamToken{Token: iamToken.IamToken, ExpiresAt: iamToken.ExpiresAt.AsTime()}, nil
+}
+
+func (lb *YandexCloudCreator) Now() time.Time {
+	return time.Now()
+}
+
+func buildSDK(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*ycsdk.SDK, error) {
+	creds, err := ycsdk.ServiceAccountKey(authorizedKey)
+	if err != nil {
+		return nil, err
+	}
+
+	sdk, err := ycsdk.Build(ctx, ycsdk.Config{
+		Credentials: creds,
+		Endpoint:    apiEndpoint,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return sdk, nil
+}
+
+func closeSDK(ctx context.Context, sdk *ycsdk.SDK) error {
+	return sdk.Shutdown(ctx)
+}
+
+// Implementation of LockboxClient.
+type LockboxClient struct {
+	lockboxPayloadClient lockbox.PayloadServiceClient
+}
+
+func (lc *LockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
+	payload, err := lc.lockboxPayloadClient.Get(
+		ctx,
+		&lockbox.GetPayloadRequest{
+			SecretId:  secretID,
+			VersionId: versionID,
+		},
+		grpc.PerRPCCredentials(perRPCCredentials{iamToken: iamToken}),
+	)
+	if err != nil {
+		return nil, err
+	}
+	return payload.Entries, nil
+}
+
+type perRPCCredentials struct {
+	iamToken string
+}
+
+func (t perRPCCredentials) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
+	return map[string]string{"Authorization": "Bearer " + t.iamToken}, nil
+}
+
+func (perRPCCredentials) RequireTransportSecurity() bool {
+	return true
+}

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

@@ -0,0 +1,299 @@
+/*
+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 lockbox
+
+import (
+	"context"
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	ctrl "sigs.k8s.io/controller-runtime"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"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"
+)
+
+const maxSecretsClientLifetime = 5 * time.Minute // supposed SecretsClient lifetime is quite short
+const iamTokenCleanupDelay = 1 * time.Hour       // specifies how often cleanUpIamTokenMap() is performed
+
+var log = ctrl.Log.WithName("provider").WithName("yandex").WithName("lockbox")
+
+type iamTokenKey struct {
+	authorizedKeyID  string
+	serviceAccountID string
+	privateKeyHash   string
+}
+
+// lockboxProvider is a provider for Yandex Lockbox.
+type lockboxProvider struct {
+	yandexCloudCreator client.YandexCloudCreator
+
+	lockboxClientMap      map[string]client.LockboxClient // apiEndpoint -> LockboxClient
+	lockboxClientMapMutex sync.Mutex
+	iamTokenMap           map[iamTokenKey]*client.IamToken
+	iamTokenMapMutex      sync.Mutex
+}
+
+func newLockboxProvider(yandexCloudCreator client.YandexCloudCreator) *lockboxProvider {
+	return &lockboxProvider{
+		yandexCloudCreator: yandexCloudCreator,
+		lockboxClientMap:   make(map[string]client.LockboxClient),
+		iamTokenMap:        make(map[iamTokenKey]*client.IamToken),
+	}
+}
+
+// NewClient constructs a Yandex Lockbox Provider.
+func (p *lockboxProvider) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.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")
+	}
+	storeSpecYandexLockbox := storeSpec.Provider.YandexLockbox
+
+	authorizedKeySecretName := storeSpecYandexLockbox.Auth.AuthorizedKey.Name
+	if authorizedKeySecretName == "" {
+		return nil, fmt.Errorf("invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
+	}
+	objectKey := types.NamespacedName{
+		Name:      authorizedKeySecretName,
+		Namespace: namespace,
+	}
+
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
+		if storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace == nil {
+			return nil, fmt.Errorf("invalid ClusterSecretStore: missing AuthorizedKey Namespace")
+		}
+		objectKey.Namespace = *storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace
+	}
+
+	authorizedKeySecret := &corev1.Secret{}
+	err := kube.Get(ctx, objectKey, authorizedKeySecret)
+	if err != nil {
+		return nil, fmt.Errorf("could not fetch AuthorizedKey secret: %w", err)
+	}
+
+	authorizedKeySecretData := authorizedKeySecret.Data[storeSpecYandexLockbox.Auth.AuthorizedKey.Key]
+	if (authorizedKeySecretData == nil) || (len(authorizedKeySecretData) == 0) {
+		return nil, fmt.Errorf("missing AuthorizedKey")
+	}
+
+	var authorizedKey iamkey.Key
+	err = json.Unmarshal(authorizedKeySecretData, &authorizedKey)
+	if err != nil {
+		return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
+	}
+
+	lockboxClient, err := p.getOrCreateLockboxClient(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create Yandex Lockbox client: %w", err)
+	}
+
+	iamToken, err := p.getOrCreateIamToken(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create IAM token: %w", err)
+	}
+
+	return &lockboxSecretsClient{lockboxClient, iamToken.Token}, nil
+}
+
+func (p *lockboxProvider) getOrCreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
+	p.lockboxClientMapMutex.Lock()
+	defer p.lockboxClientMapMutex.Unlock()
+
+	if _, ok := p.lockboxClientMap[apiEndpoint]; !ok {
+		log.Info("creating LockboxClient", "apiEndpoint", apiEndpoint)
+
+		lockboxClient, err := p.yandexCloudCreator.CreateLockboxClient(ctx, apiEndpoint, authorizedKey)
+		if err != nil {
+			return nil, err
+		}
+		p.lockboxClientMap[apiEndpoint] = lockboxClient
+	}
+	return p.lockboxClientMap[apiEndpoint], nil
+}
+
+func (p *lockboxProvider) getOrCreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
+	p.iamTokenMapMutex.Lock()
+	defer p.iamTokenMapMutex.Unlock()
+
+	iamTokenKey := buildIamTokenKey(authorizedKey)
+	if iamToken, ok := p.iamTokenMap[iamTokenKey]; !ok || !p.isIamTokenUsable(iamToken) {
+		log.Info("creating IAM token", "authorizedKeyId", authorizedKey.Id)
+
+		iamToken, err := p.yandexCloudCreator.CreateIamToken(ctx, apiEndpoint, authorizedKey)
+		if err != nil {
+			return nil, err
+		}
+
+		log.Info("created IAM token", "authorizedKeyId", authorizedKey.Id, "expiresAt", iamToken.ExpiresAt)
+
+		p.iamTokenMap[iamTokenKey] = iamToken
+	}
+	return p.iamTokenMap[iamTokenKey], nil
+}
+
+func (p *lockboxProvider) isIamTokenUsable(iamToken *client.IamToken) bool {
+	now := p.yandexCloudCreator.Now()
+	return now.Add(maxSecretsClientLifetime).Before(iamToken.ExpiresAt)
+}
+
+func buildIamTokenKey(authorizedKey *iamkey.Key) iamTokenKey {
+	privateKeyHash := sha256.Sum256([]byte(authorizedKey.PrivateKey))
+	return iamTokenKey{
+		authorizedKey.GetId(),
+		authorizedKey.GetServiceAccountId(),
+		hex.EncodeToString(privateKeyHash[:]),
+	}
+}
+
+// Used for testing.
+func (p *lockboxProvider) isIamTokenCached(authorizedKey *iamkey.Key) bool {
+	p.iamTokenMapMutex.Lock()
+	defer p.iamTokenMapMutex.Unlock()
+
+	_, ok := p.iamTokenMap[buildIamTokenKey(authorizedKey)]
+	return ok
+}
+
+func (p *lockboxProvider) cleanUpIamTokenMap() {
+	p.iamTokenMapMutex.Lock()
+	defer p.iamTokenMapMutex.Unlock()
+
+	for key, value := range p.iamTokenMap {
+		if p.yandexCloudCreator.Now().After(value.ExpiresAt) {
+			log.Info("deleting IAM token", "authorizedKeyId", key.authorizedKeyID)
+			delete(p.iamTokenMap, key)
+		}
+	}
+}
+
+// lockboxSecretsClient is a secrets client for Yandex Lockbox.
+type lockboxSecretsClient struct {
+	lockboxClient client.LockboxClient
+	iamToken      string
+}
+
+// GetSecret returns a single secret from the provider.
+func (c *lockboxSecretsClient) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
+	if err != nil {
+		return nil, fmt.Errorf("unable to request secret payload to get secret: %w", err)
+	}
+
+	if ref.Property == "" {
+		keyToValue := make(map[string]interface{}, len(entries))
+		for _, entry := range entries {
+			value, err := getValueAsIs(entry)
+			if err != nil {
+				return nil, err
+			}
+			keyToValue[entry.Key] = value
+		}
+		out, err := json.Marshal(keyToValue)
+		if err != nil {
+			return nil, fmt.Errorf("failed to marshal secret: %w", err)
+		}
+		return out, nil
+	}
+
+	entry, err := findEntryByKey(entries, ref.Property)
+	if err != nil {
+		return nil, err
+	}
+	return getValueAsBinary(entry)
+}
+
+// GetSecretMap returns multiple k/v pairs from the provider.
+func (c *lockboxSecretsClient) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
+	if err != nil {
+		return nil, fmt.Errorf("unable to request secret payload to get secret map: %w", err)
+	}
+
+	secretMap := make(map[string][]byte, len(entries))
+	for _, entry := range entries {
+		value, err := getValueAsBinary(entry)
+		if err != nil {
+			return nil, err
+		}
+		secretMap[entry.Key] = value
+	}
+	return secretMap, nil
+}
+
+func (c *lockboxSecretsClient) Close(ctx context.Context) error {
+	return nil
+}
+
+func getValueAsIs(entry *lockbox.Payload_Entry) (interface{}, error) {
+	switch entry.Value.(type) {
+	case *lockbox.Payload_Entry_TextValue:
+		return entry.GetTextValue(), nil
+	case *lockbox.Payload_Entry_BinaryValue:
+		return entry.GetBinaryValue(), nil
+	default:
+		return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
+	}
+}
+
+func getValueAsBinary(entry *lockbox.Payload_Entry) ([]byte, error) {
+	switch entry.Value.(type) {
+	case *lockbox.Payload_Entry_TextValue:
+		return []byte(entry.GetTextValue()), nil
+	case *lockbox.Payload_Entry_BinaryValue:
+		return entry.GetBinaryValue(), nil
+	default:
+		return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
+	}
+}
+
+func findEntryByKey(entries []*lockbox.Payload_Entry, key string) (*lockbox.Payload_Entry, error) {
+	for i := range entries {
+		if entries[i].Key == key {
+			return entries[i], nil
+		}
+	}
+	return nil, fmt.Errorf("payload entry with key '%s' not found", key)
+}
+
+func init() {
+	lockboxProvider := newLockboxProvider(&grpc.YandexCloudCreator{})
+
+	go func() {
+		for {
+			time.Sleep(iamTokenCleanupDelay)
+			lockboxProvider.cleanUpIamTokenMap()
+		}
+	}()
+
+	schema.Register(
+		lockboxProvider,
+		&esv1alpha1.SecretStoreProvider{
+			YandexLockbox: &esv1alpha1.YandexLockboxProvider{},
+		},
+	)
+}

+ 677 - 0
pkg/provider/yandex/lockbox/lockbox_test.go

@@ -0,0 +1,677 @@
+/*
+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 lockbox
+
+import (
+	"context"
+	b64 "encoding/base64"
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/google/uuid"
+	tassert "github.com/stretchr/testify/assert"
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	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"
+)
+
+func TestNewClient(t *testing.T) {
+	ctx := context.Background()
+	const namespace = "namespace"
+
+	store := &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				YandexLockbox: &esv1alpha1.YandexLockboxProvider{},
+			},
+		},
+	}
+	provider, err := schema.GetProvider(store)
+	tassert.Nil(t, err)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	secretClient, err := provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
+	tassert.Nil(t, secretClient)
+
+	store.Spec.Provider.YandexLockbox.Auth = esv1alpha1.YandexLockboxAuth{}
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
+	tassert.Nil(t, secretClient)
+
+	store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey = esmeta.SecretKeySelector{}
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
+	tassert.Nil(t, secretClient)
+
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey.Name = authorizedKeySecretName
+	store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey.Key = authorizedKeySecretKey
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "could not fetch AuthorizedKey secret: secrets \"authorizedKeySecretName\" not found")
+	tassert.Nil(t, secretClient)
+
+	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, newFakeAuthorizedKey())
+	tassert.Nil(t, err)
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "failed to create Yandex Lockbox client: private key parsing failed: Invalid Key: Key must be PEM encoded PKCS1 or PKCS8 private key")
+	tassert.Nil(t, secretClient)
+}
+
+func TestGetSecretForAllEntries(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	k1, v1 := "k1", "v1"
+	k2, v2 := "k2", []byte("v2")
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+		binaryEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(
+		t,
+		map[string]string{
+			k1: v1,
+			k2: base64(v2),
+		},
+		unmarshalStringMap(t, data),
+	)
+}
+
+func TestGetSecretForTextEntry(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	k1, v1 := "k1", "v1"
+	k2, v2 := "k2", []byte("v2")
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+		binaryEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
+	tassert.Nil(t, err)
+
+	tassert.Equal(t, v1, string(data))
+}
+
+func TestGetSecretForBinaryEntry(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	k1, v1 := "k1", "v1"
+	k2, v2 := "k2", []byte("v2")
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+		binaryEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k2})
+	tassert.Nil(t, err)
+
+	tassert.Equal(t, v2, data)
+}
+
+func TestGetSecretByVersionID(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	oldKey, oldVal := "oldKey", "oldVal"
+	secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(oldKey, oldVal),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(t, map[string]string{oldKey: oldVal}, unmarshalStringMap(t, data))
+
+	newKey, newVal := "newKey", "newVal"
+	newVersionID := lockboxBackend.AddVersion(secretID,
+		textEntry(newKey, newVal),
+	)
+
+	data, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(t, map[string]string{oldKey: oldVal}, unmarshalStringMap(t, data))
+
+	data, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: newVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(t, map[string]string{newKey: newVal}, unmarshalStringMap(t, data))
+}
+
+func TestGetSecretUnauthorized(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKeyA := newFakeAuthorizedKey()
+	authorizedKeyB := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKeyA,
+		textEntry("k1", "v1"),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKeyB)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
+}
+
+func TestGetSecretNotFound(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: "no-secret-with-this-id"})
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
+
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry("k1", "v1"),
+	)
+	_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: "no-version-with-this-id"})
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: version not found")
+}
+
+func TestGetSecretWithTwoNamespaces(t *testing.T) {
+	ctx := context.Background()
+	namespace1 := uuid.NewString()
+	namespace2 := uuid.NewString()
+	authorizedKey1 := newFakeAuthorizedKey()
+	authorizedKey2 := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	k1, v1 := "k1", "v1"
+	secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
+		textEntry(k1, v1),
+	)
+	k2, v2 := "k2", "v2"
+	secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
+		textEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace1, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey1)
+	tassert.Nil(t, err)
+	err = createK8sSecret(ctx, k8sClient, namespace2, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey2)
+	tassert.Nil(t, err)
+	store1 := newYandexLockboxSecretStore("", namespace1, authorizedKeySecretName, authorizedKeySecretKey)
+	store2 := newYandexLockboxSecretStore("", namespace2, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient1, err := provider.NewClient(ctx, store1, k8sClient, namespace1)
+	tassert.Nil(t, err)
+	secretsClient2, err := provider.NewClient(ctx, store2, k8sClient, namespace2)
+	tassert.Nil(t, err)
+
+	data, err := secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
+	tassert.Equal(t, v1, string(data))
+	tassert.Nil(t, err)
+	data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
+
+	data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
+	data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
+	tassert.Equal(t, v2, string(data))
+	tassert.Nil(t, err)
+}
+
+func TestGetSecretWithTwoApiEndpoints(t *testing.T) {
+	ctx := context.Background()
+	apiEndpoint1 := uuid.NewString()
+	apiEndpoint2 := uuid.NewString()
+	namespace := uuid.NewString()
+	authorizedKey1 := newFakeAuthorizedKey()
+	authorizedKey2 := newFakeAuthorizedKey()
+
+	lockboxBackend1 := fake.NewLockboxBackend(time.Hour)
+	k1, v1 := "k1", "v1"
+	secretID1, _ := lockboxBackend1.CreateSecret(authorizedKey1,
+		textEntry(k1, v1),
+	)
+	lockboxBackend2 := fake.NewLockboxBackend(time.Hour)
+	k2, v2 := "k2", "v2"
+	secretID2, _ := lockboxBackend2.CreateSecret(authorizedKey2,
+		textEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName1 = "authorizedKeySecretName1"
+	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
+	tassert.Nil(t, err)
+	const authorizedKeySecretName2 = "authorizedKeySecretName2"
+	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
+	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
+	tassert.Nil(t, err)
+
+	store1 := newYandexLockboxSecretStore(apiEndpoint1, namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
+	store2 := newYandexLockboxSecretStore(apiEndpoint2, namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
+
+	provider1 := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend1,
+	})
+	provider2 := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend2,
+	})
+
+	secretsClient1, err := provider1.NewClient(ctx, store1, k8sClient, namespace)
+	tassert.Nil(t, err)
+	secretsClient2, err := provider2.NewClient(ctx, store2, k8sClient, namespace)
+	tassert.Nil(t, err)
+
+	var data []byte
+
+	data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
+	tassert.Equal(t, v1, string(data))
+	tassert.Nil(t, err)
+	data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
+
+	data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
+	data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
+	tassert.Equal(t, v2, string(data))
+	tassert.Nil(t, err)
+}
+
+func TestGetSecretWithIamTokenExpiration(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	tokenExpirationTime := time.Hour
+	lockboxBackend := fake.NewLockboxBackend(tokenExpirationTime)
+	k1, v1 := "k1", "v1"
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+
+	var data []byte
+
+	oldSecretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err = oldSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
+	tassert.Equal(t, v1, string(data))
+	tassert.Nil(t, err)
+
+	lockboxBackend.AdvanceClock(2 * tokenExpirationTime)
+
+	data, err = oldSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: iam token expired")
+
+	newSecretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err = newSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
+	tassert.Equal(t, v1, string(data))
+	tassert.Nil(t, err)
+}
+
+func TestGetSecretWithIamTokenCleanup(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey1 := newFakeAuthorizedKey()
+	authorizedKey2 := newFakeAuthorizedKey()
+
+	tokenExpirationDuration := time.Hour
+	lockboxBackend := fake.NewLockboxBackend(tokenExpirationDuration)
+	secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
+		textEntry("k1", "v1"),
+	)
+	secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
+		textEntry("k2", "v2"),
+	)
+
+	var err error
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName1 = "authorizedKeySecretName1"
+	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
+	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
+	tassert.Nil(t, err)
+	const authorizedKeySecretName2 = "authorizedKeySecretName2"
+	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
+	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
+	tassert.Nil(t, err)
+
+	store1 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
+	store2 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+
+	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.isIamTokenCached(authorizedKey2))
+
+	// Access secretID1 with authorizedKey1, IAM token for authorizedKey1 should be cached
+	secretsClient, err := provider.NewClient(ctx, store1, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1})
+	tassert.Nil(t, err)
+
+	tassert.True(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.isIamTokenCached(authorizedKey2))
+
+	lockboxBackend.AdvanceClock(tokenExpirationDuration * 2)
+
+	// Access secretID2 with authorizedKey2, IAM token for authorizedKey2 should be cached
+	secretsClient, err = provider.NewClient(ctx, store2, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2})
+	tassert.Nil(t, err)
+
+	tassert.True(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+
+	lockboxBackend.AdvanceClock(tokenExpirationDuration)
+
+	tassert.True(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+
+	provider.cleanUpIamTokenMap()
+
+	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+
+	lockboxBackend.AdvanceClock(tokenExpirationDuration)
+
+	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+
+	provider.cleanUpIamTokenMap()
+
+	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.isIamTokenCached(authorizedKey2))
+}
+
+func TestGetSecretMap(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	k1, v1 := "k1", "v1"
+	k2, v2 := "k2", []byte("v2")
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+		binaryEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(
+		t,
+		map[string][]byte{
+			k1: []byte(v1),
+			k2: v2,
+		},
+		data,
+	)
+}
+
+func TestGetSecretMapByVersionID(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	oldKey, oldVal := "oldKey", "oldVal"
+	secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(oldKey, oldVal),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(t, map[string][]byte{oldKey: []byte(oldVal)}, data)
+
+	newKey, newVal := "newKey", "newVal"
+	newVersionID := lockboxBackend.AddVersion(secretID,
+		textEntry(newKey, newVal),
+	)
+
+	data, err = secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(t, map[string][]byte{oldKey: []byte(oldVal)}, data)
+
+	data, err = secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: newVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(t, map[string][]byte{newKey: []byte(newVal)}, data)
+}
+
+// helper functions
+
+func newYandexLockboxSecretStore(apiEndpoint, namespace, authorizedKeySecretName, authorizedKeySecretKey string) esv1alpha1.GenericStore {
+	return &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				YandexLockbox: &esv1alpha1.YandexLockboxProvider{
+					APIEndpoint: apiEndpoint,
+					Auth: esv1alpha1.YandexLockboxAuth{
+						AuthorizedKey: esmeta.SecretKeySelector{
+							Name: authorizedKeySecretName,
+							Key:  authorizedKeySecretKey,
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func createK8sSecret(ctx context.Context, k8sClient client.Client, namespace, secretName, secretKey string, secretContent interface{}) error {
+	data, err := json.Marshal(secretContent)
+	if err != nil {
+		return err
+	}
+
+	err = k8sClient.Create(ctx, &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+			Name:      secretName,
+		},
+		Data: map[string][]byte{secretKey: data},
+	})
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func newFakeAuthorizedKey() *iamkey.Key {
+	uniqueLabel := uuid.NewString()
+	return &iamkey.Key{
+		Id: uniqueLabel,
+		Subject: &iamkey.Key_ServiceAccountId{
+			ServiceAccountId: uniqueLabel,
+		},
+		PrivateKey: uniqueLabel,
+	}
+}
+
+func textEntry(key, value string) *lockbox.Payload_Entry {
+	return &lockbox.Payload_Entry{
+		Key: key,
+		Value: &lockbox.Payload_Entry_TextValue{
+			TextValue: value,
+		},
+	}
+}
+
+func binaryEntry(key string, value []byte) *lockbox.Payload_Entry {
+	return &lockbox.Payload_Entry{
+		Key: key,
+		Value: &lockbox.Payload_Entry_BinaryValue{
+			BinaryValue: value,
+		},
+	}
+}
+
+func unmarshalStringMap(t *testing.T, data []byte) map[string]string {
+	stringMap := make(map[string]string)
+	err := json.Unmarshal(data, &stringMap)
+	tassert.Nil(t, err)
+	return stringMap
+}
+
+func base64(data []byte) string {
+	return b64.StdEncoding.EncodeToString(data)
+}

+ 18 - 5
pkg/utils/utils.go

@@ -14,14 +14,20 @@ limitations under the License.
 
 package utils
 
-import "reflect"
+import (
+
+	// nolint:gosec
+	"crypto/md5"
+	"fmt"
+	"reflect"
+)
 
 // MergeByteMap merges map of byte slices.
-func MergeByteMap(src, dst map[string][]byte) map[string][]byte {
-	for k, v := range dst {
-		src[k] = v
+func MergeByteMap(dst, src map[string][]byte) map[string][]byte {
+	for k, v := range src {
+		dst[k] = v
 	}
-	return src
+	return dst
 }
 
 // MergeStringMap performs a deep clone from src to dest.
@@ -35,3 +41,10 @@ func MergeStringMap(dest, src map[string]string) {
 func IsNil(i interface{}) bool {
 	return i == nil || reflect.ValueOf(i).IsNil()
 }
+
+// ObjectHash calculates md5 sum of the data contained in the secret.
+// nolint:gosec
+func ObjectHash(object interface{}) string {
+	textualVersion := fmt.Sprintf("%+v", object)
+	return fmt.Sprintf("%x", md5.Sum([]byte(textualVersion)))
+}

+ 62 - 0
pkg/utils/utils_test.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 utils
+
+import (
+	"testing"
+
+	v1 "k8s.io/api/core/v1"
+)
+
+func TestObjectHash(t *testing.T) {
+	tests := []struct {
+		name  string
+		input interface{}
+		want  string
+	}{
+		{
+			name:  "A nil should be still working",
+			input: nil,
+			want:  "60046f14c917c18a9a0f923e191ba0dc",
+		},
+		{
+			name:  "We accept a simple scalar value, i.e. string",
+			input: "hello there",
+			want:  "161bc25962da8fed6d2f59922fb642aa",
+		},
+		{
+			name: "A complex object like a secret is not an issue",
+			input: v1.Secret{Data: map[string][]byte{
+				"xx": []byte("yyy"),
+			}},
+			want: "a9fe13fd43b20829b45f0a93372413dd",
+		},
+		{
+			name: "map also works",
+			input: map[string][]byte{
+				"foo": []byte("value1"),
+				"bar": []byte("value2"),
+			},
+			want: "caa0155759a6a9b3b6ada5a6883ee2bb",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := ObjectHash(tt.input); got != tt.want {
+				t.Errorf("ObjectHash() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}

+ 1 - 0
tools.go

@@ -1,3 +1,4 @@
+//go:build tools
 // +build tools
 
 package tools