瀏覽代碼

Feature/scaleway provider (#2086)

* wip: basic structure of scaleway provider

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* test: add some tests for GetAllSecrets

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: implement PushSecret

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* test: improved test fixtures

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: allow finding secrets by project using the path property

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: add delete secret method

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* Delete dupplicate of push remote ref test implem

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: add capability to use a secret for configuring access token

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: implement GetSecretMap

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: filtering by name and projetc id

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* test: add test for finding secret by name regexp

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: config validation

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* fix: handle situation where no namespace is specified and we cannot provide a default

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: reference secrets by id or name

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* fix: invalid request caused by pagination handling

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: log the error when failing to access secret version

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* fix: pass context to sdk where missing

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: add a cache for reducing AccessSecretVersion() calls

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* refacto: use GetSecret with name instead of ListSecrets

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: allow using secret name in ExternalSecrets

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: use latest_enabled instead of latest

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* refacto: optimized PushSecret and improved its test coverage

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* fix: doesConfigDependOnNamespace was always true

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: use new api with refactored name-based endpoints

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* remove useless todo

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* fix: use secret names as key for GetAllSecrets

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: support gjson propery lookup

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: e2e tests

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* test: e2e test using secret to store api key

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* test: cleanup left over resources on the secret manager before each e2e run

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* doc: add doc for scaleway provider

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* refacto: fix lint issues

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* test: cleanup code in e2e was commented

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: the previous version is disabled when we push to a secret

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* doc: add comments to ScalewayProvider struct to point to console and doc

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>

* feat: add missing e2e env vars for scaleway

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

* docs: add scaleway to support/stability table

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

---------

Signed-off-by: Julien Loctaux <no.mail@jloc.fr>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Co-authored-by: Moritz Johner <beller.moritz@googlemail.com>
azert9 3 年之前
父節點
當前提交
f181500e98
共有 33 個文件被更改,包括 2595 次插入57 次删除
  1. 5 1
      .github/workflows/e2e.yml
  2. 47 0
      apis/externalsecrets/v1beta1/secretstore_scaleway_types.go
  3. 4 0
      apis/externalsecrets/v1beta1/secretstore_types.go
  4. 50 0
      apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
  5. 74 0
      config/crds/bases/external-secrets.io_clustersecretstores.yaml
  6. 74 0
      config/crds/bases/external-secrets.io_secretstores.yaml
  7. 116 0
      deploy/crds/bundle.yaml
  8. 136 0
      docs/api/spec.md
  9. 2 0
      docs/introduction/stability-support.md
  10. 50 0
      docs/provider/scaleway.md
  11. 4 1
      e2e/framework/framework.go
  12. 1 0
      e2e/go.mod
  13. 2 0
      e2e/go.sum
  14. 5 0
      e2e/run.sh
  15. 66 45
      e2e/suites/provider/cases/common/common.go
  16. 6 6
      e2e/suites/provider/cases/common/find_by_name.go
  17. 3 3
      e2e/suites/provider/cases/common/find_by_tags.go
  18. 1 0
      e2e/suites/provider/cases/import.go
  19. 56 0
      e2e/suites/provider/cases/scaleway/config.go
  20. 106 0
      e2e/suites/provider/cases/scaleway/provider.go
  21. 121 0
      e2e/suites/provider/cases/scaleway/scaleway.go
  22. 1 0
      go.mod
  23. 3 1
      go.sum
  24. 1 0
      hack/api-docs/mkdocs.yml
  25. 1 0
      pkg/generator/register/register.go
  26. 2 0
      pkg/provider/register/register.go
  27. 97 0
      pkg/provider/scaleway/cache.go
  28. 63 0
      pkg/provider/scaleway/cache_test.go
  29. 444 0
      pkg/provider/scaleway/client.go
  30. 386 0
      pkg/provider/scaleway/client_test.go
  31. 441 0
      pkg/provider/scaleway/fake_secret_api_test.go
  32. 195 0
      pkg/provider/scaleway/provider.go
  33. 32 0
      pkg/provider/scaleway/secret_api.go

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

@@ -36,7 +36,11 @@ env:
   AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET}}
   TENANT_ID: ${{ secrets.TENANT_ID}}
   VAULT_URL: ${{ secrets.VAULT_URL}}
-
+  SCALEWAY_API_URL: ${{ secrets.SCALEWAY_API_URL }}
+  SCALEWAY_REGION: ${{ secrets.SCALEWAY_REGION }}
+  SCALEWAY_PROJECT_ID: ${{ secrets.SCALEWAY_PROJECT_ID }}
+  SCALEWAY_ACCESS_KEY: ${{ secrets.SCALEWAY_ACCESS_KEY }}
+  SCALEWAY_SECRET_KEY: ${{ secrets.SCALEWAY_SECRET_KEY }}
 
 jobs:
 

+ 47 - 0
apis/externalsecrets/v1beta1/secretstore_scaleway_types.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 v1beta1
+
+import esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+
+type ScalewayProviderSecretRef struct {
+
+	// Value can be specified directly to set a value without using a secret.
+	// +optional
+	Value string `json:"value,omitempty"`
+
+	// SecretRef references a key in a secret that will be used as value.
+	// +optional
+	SecretRef *esmeta.SecretKeySelector `json:"secretRef,omitempty"`
+}
+
+type ScalewayProvider struct {
+
+	// APIURL is the url of the api to use. Defaults to https://api.scaleway.com
+	// +optional
+	APIURL string `json:"apiUrl,omitempty"`
+
+	// Region where your secrets are located: https://developers.scaleway.com/en/quickstart/#region-and-zone
+	Region string `json:"region"`
+
+	// ProjectID is the id of your project, which you can find in the console: https://console.scaleway.com/project/settings
+	ProjectID string `json:"projectId"`
+
+	// AccessKey is the non-secret part of the api key.
+	AccessKey *ScalewayProviderSecretRef `json:"accessKey"`
+
+	// SecretKey is the non-secret part of the api key.
+	SecretKey *ScalewayProviderSecretRef `json:"secretKey"`
+}

+ 4 - 0
apis/externalsecrets/v1beta1/secretstore_types.go

@@ -121,6 +121,10 @@ type SecretStoreProvider struct {
 	// +optional
 	Senhasegura *SenhaseguraProvider `json:"senhasegura,omitempty"`
 
+	// Scaleway
+	// +optional
+	Scaleway *ScalewayProvider `json:"scaleway,omitempty"`
+
 	// Doppler configures this store to sync secrets using the Doppler provider
 	// +optional
 	Doppler *DopplerProvider `json:"doppler,omitempty"`

+ 50 - 0
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -1520,6 +1520,51 @@ func (in *OracleSecretRef) DeepCopy() *OracleSecretRef {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ScalewayProvider) DeepCopyInto(out *ScalewayProvider) {
+	*out = *in
+	if in.AccessKey != nil {
+		in, out := &in.AccessKey, &out.AccessKey
+		*out = new(ScalewayProviderSecretRef)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.SecretKey != nil {
+		in, out := &in.SecretKey, &out.SecretKey
+		*out = new(ScalewayProviderSecretRef)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScalewayProvider.
+func (in *ScalewayProvider) DeepCopy() *ScalewayProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(ScalewayProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ScalewayProviderSecretRef) DeepCopyInto(out *ScalewayProviderSecretRef) {
+	*out = *in
+	if in.SecretRef != nil {
+		in, out := &in.SecretRef, &out.SecretRef
+		*out = new(metav1.SecretKeySelector)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScalewayProviderSecretRef.
+func (in *ScalewayProviderSecretRef) DeepCopy() *ScalewayProviderSecretRef {
+	if in == nil {
+		return nil
+	}
+	out := new(ScalewayProviderSecretRef)
+	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
@@ -1662,6 +1707,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(SenhaseguraProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Scaleway != nil {
+		in, out := &in.Scaleway, &out.Scaleway
+		*out = new(ScalewayProvider)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.Doppler != nil {
 		in, out := &in.Doppler, &out.Doppler
 		*out = new(DopplerProvider)

+ 74 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -2749,6 +2749,80 @@ spec:
                     - region
                     - vault
                     type: object
+                  scaleway:
+                    description: Scaleway
+                    properties:
+                      accessKey:
+                        description: AccessKey is the non-secret part of the api key.
+                        properties:
+                          secretRef:
+                            description: SecretRef references a key in a secret that
+                              will be used as value.
+                            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
+                          value:
+                            description: Value can be specified directly to set a
+                              value without using a secret.
+                            type: string
+                        type: object
+                      apiUrl:
+                        description: APIURL is the url of the api to use. Defaults
+                          to https://api.scaleway.com
+                        type: string
+                      projectId:
+                        description: 'ProjectID is the id of your project, which you
+                          can find in the console: https://console.scaleway.com/project/settings'
+                        type: string
+                      region:
+                        description: 'Region where your secrets are located: https://developers.scaleway.com/en/quickstart/#region-and-zone'
+                        type: string
+                      secretKey:
+                        description: SecretKey is the non-secret part of the api key.
+                        properties:
+                          secretRef:
+                            description: SecretRef references a key in a secret that
+                              will be used as value.
+                            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
+                          value:
+                            description: Value can be specified directly to set a
+                              value without using a secret.
+                            type: string
+                        type: object
+                    required:
+                    - accessKey
+                    - projectId
+                    - region
+                    - secretKey
+                    type: object
                   senhasegura:
                     description: Senhasegura configures this store to sync secrets
                       using senhasegura provider

+ 74 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -2749,6 +2749,80 @@ spec:
                     - region
                     - vault
                     type: object
+                  scaleway:
+                    description: Scaleway
+                    properties:
+                      accessKey:
+                        description: AccessKey is the non-secret part of the api key.
+                        properties:
+                          secretRef:
+                            description: SecretRef references a key in a secret that
+                              will be used as value.
+                            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
+                          value:
+                            description: Value can be specified directly to set a
+                              value without using a secret.
+                            type: string
+                        type: object
+                      apiUrl:
+                        description: APIURL is the url of the api to use. Defaults
+                          to https://api.scaleway.com
+                        type: string
+                      projectId:
+                        description: 'ProjectID is the id of your project, which you
+                          can find in the console: https://console.scaleway.com/project/settings'
+                        type: string
+                      region:
+                        description: 'Region where your secrets are located: https://developers.scaleway.com/en/quickstart/#region-and-zone'
+                        type: string
+                      secretKey:
+                        description: SecretKey is the non-secret part of the api key.
+                        properties:
+                          secretRef:
+                            description: SecretRef references a key in a secret that
+                              will be used as value.
+                            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
+                          value:
+                            description: Value can be specified directly to set a
+                              value without using a secret.
+                            type: string
+                        type: object
+                    required:
+                    - accessKey
+                    - projectId
+                    - region
+                    - secretKey
+                    type: object
                   senhasegura:
                     description: Senhasegura configures this store to sync secrets
                       using senhasegura provider

+ 116 - 0
deploy/crds/bundle.yaml

@@ -2462,6 +2462,64 @@ spec:
                         - region
                         - vault
                       type: object
+                    scaleway:
+                      description: Scaleway
+                      properties:
+                        accessKey:
+                          description: AccessKey is the non-secret part of the api key.
+                          properties:
+                            secretRef:
+                              description: SecretRef references a key in a secret that will be used as value.
+                              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
+                            value:
+                              description: Value can be specified directly to set a value without using a secret.
+                              type: string
+                          type: object
+                        apiUrl:
+                          description: APIURL is the url of the api to use. Defaults to https://api.scaleway.com
+                          type: string
+                        projectId:
+                          description: 'ProjectID is the id of your project, which you can find in the console: https://console.scaleway.com/project/settings'
+                          type: string
+                        region:
+                          description: 'Region where your secrets are located: https://developers.scaleway.com/en/quickstart/#region-and-zone'
+                          type: string
+                        secretKey:
+                          description: SecretKey is the non-secret part of the api key.
+                          properties:
+                            secretRef:
+                              description: SecretRef references a key in a secret that will be used as value.
+                              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
+                            value:
+                              description: Value can be specified directly to set a value without using a secret.
+                              type: string
+                          type: object
+                      required:
+                        - accessKey
+                        - projectId
+                        - region
+                        - secretKey
+                      type: object
                     senhasegura:
                       description: Senhasegura configures this store to sync secrets using senhasegura provider
                       properties:
@@ -5829,6 +5887,64 @@ spec:
                         - region
                         - vault
                       type: object
+                    scaleway:
+                      description: Scaleway
+                      properties:
+                        accessKey:
+                          description: AccessKey is the non-secret part of the api key.
+                          properties:
+                            secretRef:
+                              description: SecretRef references a key in a secret that will be used as value.
+                              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
+                            value:
+                              description: Value can be specified directly to set a value without using a secret.
+                              type: string
+                          type: object
+                        apiUrl:
+                          description: APIURL is the url of the api to use. Defaults to https://api.scaleway.com
+                          type: string
+                        projectId:
+                          description: 'ProjectID is the id of your project, which you can find in the console: https://console.scaleway.com/project/settings'
+                          type: string
+                        region:
+                          description: 'Region where your secrets are located: https://developers.scaleway.com/en/quickstart/#region-and-zone'
+                          type: string
+                        secretKey:
+                          description: SecretKey is the non-secret part of the api key.
+                          properties:
+                            secretRef:
+                              description: SecretRef references a key in a secret that will be used as value.
+                              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
+                            value:
+                              description: Value can be specified directly to set a value without using a secret.
+                              type: string
+                          type: object
+                      required:
+                        - accessKey
+                        - projectId
+                        - region
+                        - secretKey
+                      type: object
                     senhasegura:
                       description: Senhasegura configures this store to sync secrets using senhasegura provider
                       properties:

+ 136 - 0
docs/api/spec.md

@@ -3955,6 +3955,128 @@ External Secrets meta/v1.SecretKeySelector
 <p>
 <p>This interface is to allow using v1alpha1 content in Provider registered in v1beta1.</p>
 </p>
+<h3 id="external-secrets.io/v1beta1.ScalewayProvider">ScalewayProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>apiUrl</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>APIURL is the url of the api to use. Defaults to <a href="https://api.scaleway.com">https://api.scaleway.com</a></p>
+</td>
+</tr>
+<tr>
+<td>
+<code>region</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Region where your secrets are located: <a href="https://developers.scaleway.com/en/quickstart/#region-and-zone">https://developers.scaleway.com/en/quickstart/#region-and-zone</a></p>
+</td>
+</tr>
+<tr>
+<td>
+<code>projectId</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>ProjectID is the id of your project, which you can find in the console: <a href="https://console.scaleway.com/project/settings">https://console.scaleway.com/project/settings</a></p>
+</td>
+</tr>
+<tr>
+<td>
+<code>accessKey</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.ScalewayProviderSecretRef">
+ScalewayProviderSecretRef
+</a>
+</em>
+</td>
+<td>
+<p>AccessKey is the non-secret part of the api key.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>secretKey</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.ScalewayProviderSecretRef">
+ScalewayProviderSecretRef
+</a>
+</em>
+</td>
+<td>
+<p>SecretKey is the non-secret part of the api key.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.ScalewayProviderSecretRef">ScalewayProviderSecretRef
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.ScalewayProvider">ScalewayProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>value</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Value can be specified directly to set a value without using a secret.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>secretRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>SecretRef references a key in a secret that will be used as value.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.SecretStore">SecretStore
 </h3>
 <p>
@@ -4364,6 +4486,20 @@ SenhaseguraProvider
 </tr>
 <tr>
 <td>
+<code>scaleway</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.ScalewayProvider">
+ScalewayProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Scaleway</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>doppler</code></br>
 <em>
 <a href="#external-secrets.io/v1beta1.DopplerProvider">

+ 2 - 0
docs/introduction/stability-support.md

@@ -41,6 +41,7 @@ The following table describes the stability level of each provider and who's res
 | [senhasegura DevOps Secrets Management (DSM)](https://external-secrets.io/latest/provider/senhasegura-dsm) |   alpha   |                                                                                                                                                    [@lfraga](https://github.com/lfraga) |
 | [Doppler SecretOps Platform](https://external-secrets.io/latest/provider/doppler)                          |   alpha   |                                                                                         [@ryan-blunden](https://github.com/ryan-blunden/) [@nmanoogian](https://github.com/nmanoogian/) |
 | [Keeper Security](https://www.keepersecurity.com/)                                                         |   alpha   |                                                                                                                                              [@ppodevlab](https://github.com/ppodevlab) |
+| [Scaleway](https://external-secrets.io/latest/provider/scaleway)                                           |   alpha   |                                                                                                                                                   [@azert9](https://github.com/azert9/) |
 
 ## Provider Feature Support
 
@@ -65,6 +66,7 @@ The following table show the support for features across different providers.
 | senhasegura DSM           |              |              |                      |                         |        x         |             |                             |
 | Doppler                   |      x       |              |                      |                         |        x         |             |                             |
 | Keeper Security           |      x       |              |                      |                         |        x         |      x      |                             |
+| Scaleway                  |      x       |      x       |                      |                         |        x         |      x      |              x              |
 
 ## Support Policy
 

+ 50 - 0
docs/provider/scaleway.md

@@ -0,0 +1,50 @@
+## Scaleway Secret Manager
+
+External Secrets Operator integrates with [Scaleway's Secret Manager](https://developers.scaleway.com/en/products/secret_manager/api/v1alpha1/).
+
+### Creating a SecretStore
+
+You need an api key (access key + secret key) to authenticate with the secret manager.
+Both access and secret keys can be specified either directly in the config, or by referencing
+a kubernetes secret.
+
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: secret-store
+spec:
+  provider:
+    scaleway:
+      region: <REGION>
+      projectId: <PROJECT_UUID>
+      accessKey:
+        value: <ACCESS_KEY>
+      secretKey:
+        secretRef:
+          name: <NAME_OF_KUBE_SECRET>
+          key: <KEY_IN_KUBE_SECRET>
+```
+
+### Referencing Secrets
+
+Secrets can be referenced by name or by id, using the prefixes `"name:"` and `"id:"` respectively.
+
+A PushSecret resource can only use a name reference.
+
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+    name: secret
+spec:
+    refreshInterval: 20s
+    secretStoreRef:
+        kind: SecretStore
+        name: secret-store
+    data:
+      - secretKey: <KEY_IN_KUBE_SECRET>
+        remoteRef:
+          key: id:<SECRET_UUID>
+          version: latest_enabled
+```

+ 4 - 1
e2e/framework/framework.go

@@ -46,12 +46,15 @@ type Framework struct {
 	Namespace *api.Namespace
 
 	Addons []addon.Addon
+
+	MakeRemoteRefKey func(base string) string
 }
 
 // New returns a new framework instance with defaults.
 func New(baseName string) *Framework {
 	f := &Framework{
-		BaseName: baseName,
+		BaseName:         baseName,
+		MakeRemoteRefKey: func(base string) string { return base },
 	}
 	f.KubeConfig, f.KubeClientSet, f.CRClient = util.NewConfig()
 

+ 1 - 0
e2e/go.mod

@@ -55,6 +55,7 @@ require (
 	github.com/onsi/ginkgo/v2 v2.9.1
 	github.com/onsi/gomega v1.27.3
 	github.com/oracle/oci-go-sdk/v56 v56.1.0
+	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.13.0.20230227165516-144b3e06ecf2
 	github.com/xanzy/go-gitlab v0.81.0
 	golang.org/x/oauth2 v0.6.0
 	google.golang.org/api v0.112.0

+ 2 - 0
e2e/go.sum

@@ -859,6 +859,8 @@ github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFo
 github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
 github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.13.0.20230227165516-144b3e06ecf2 h1:LyHBuaec79FY+tmMHUdnctd/es78NzZ7VKcE2QmdPTc=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.13.0.20230227165516-144b3e06ecf2/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=

+ 5 - 0
e2e/run.sh

@@ -73,6 +73,11 @@ kubectl run --rm \
   --env="ORACLE_REGION=${ORACLE_REGION:-}" \
   --env="ORACLE_FINGERPRINT=${ORACLE_FINGERPRINT:-}" \
   --env="ORACLE_KEY=${ORACLE_KEY:-}" \
+  --env="SCALEWAY_API_URL=${SCALEWAY_API_URL:-}" \
+  --env="SCALEWAY_REGION=${SCALEWAY_REGION:-}" \
+  --env="SCALEWAY_PROJECT_ID=${SCALEWAY_PROJECT_ID:-}" \
+  --env="SCALEWAY_ACCESS_KEY=${SCALEWAY_ACCESS_KEY:-}" \
+  --env="SCALEWAY_SECRET_KEY=${SCALEWAY_SECRET_KEY:-}" \
   --env="VERSION=${VERSION}" \
   --env="TEST_SUITES=${TEST_SUITES}" \
   --overrides='{ "apiVersion": "v1", "spec":{"serviceAccountName": "external-secrets-e2e"}}' \

+ 66 - 45
e2e/suites/provider/cases/common/common.go

@@ -87,10 +87,12 @@ func SimpleDataSync(f *framework.Framework) (string, func(*framework.TestCase))
 	return "[common] should sync simple secrets from .Data[]", func(tc *framework.TestCase) {
 		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
 		secretKey2 := fmt.Sprintf("%s-%s", f.Namespace.Name, "other")
+		remoteRefKey1 := f.MakeRemoteRefKey(secretKey1)
+		remoteRefKey2 := f.MakeRemoteRefKey(secretKey2)
 		secretValue := "bar"
 		tc.Secrets = map[string]framework.SecretEntry{
-			secretKey1: {Value: secretValue},
-			secretKey2: {Value: secretValue},
+			remoteRefKey1: {Value: secretValue},
+			remoteRefKey2: {Value: secretValue},
 		}
 		tc.ExpectedSecret = &v1.Secret{
 			Type: v1.SecretTypeOpaque,
@@ -103,13 +105,13 @@ func SimpleDataSync(f *framework.Framework) (string, func(*framework.TestCase))
 			{
 				SecretKey: secretKey1,
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key: secretKey1,
+					Key: remoteRefKey1,
 				},
 			},
 			{
 				SecretKey: secretKey2,
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key: secretKey2,
+					Key: remoteRefKey2,
 				},
 			},
 		}
@@ -121,9 +123,10 @@ func SimpleDataSync(f *framework.Framework) (string, func(*framework.TestCase))
 func SyncWithoutTargetName(f *framework.Framework) (string, func(*framework.TestCase)) {
 	return "[common] should sync with empty target name.", func(tc *framework.TestCase) {
 		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
+		remoteRefKey1 := f.MakeRemoteRefKey(secretKey1)
 		secretValue := "bar"
 		tc.Secrets = map[string]framework.SecretEntry{
-			secretKey1: {Value: secretValue},
+			remoteRefKey1: {Value: secretValue},
 		}
 		tc.ExpectedSecret = &v1.Secret{
 			Type: v1.SecretTypeOpaque,
@@ -136,7 +139,7 @@ func SyncWithoutTargetName(f *framework.Framework) (string, func(*framework.Test
 			{
 				SecretKey: secretKey1,
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key: secretKey1,
+					Key: remoteRefKey1,
 				},
 			},
 		}
@@ -149,9 +152,11 @@ func JSONDataWithProperty(f *framework.Framework) (string, func(*framework.TestC
 	return "[common] should sync multiple secrets from .Data[]", func(tc *framework.TestCase) {
 		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
 		secretKey2 := fmt.Sprintf("%s-%s", f.Namespace.Name, "two")
+		remoteRefKey1 := f.MakeRemoteRefKey(secretKey1)
+		remoteRefKey2 := f.MakeRemoteRefKey(secretKey2)
 		tc.Secrets = map[string]framework.SecretEntry{
-			secretKey1: {Value: secretValue1},
-			secretKey2: {Value: secretValue2},
+			remoteRefKey1: {Value: secretValue1},
+			remoteRefKey2: {Value: secretValue2},
 		}
 		tc.ExpectedSecret = &v1.Secret{
 			Type: v1.SecretTypeOpaque,
@@ -164,14 +169,14 @@ func JSONDataWithProperty(f *framework.Framework) (string, func(*framework.TestC
 			{
 				SecretKey: secretKey1,
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key:      secretKey1,
+					Key:      remoteRefKey1,
 					Property: "foo1",
 				},
 			},
 			{
 				SecretKey: secretKey2,
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key:      secretKey2,
+					Key:      remoteRefKey2,
 					Property: "bar2",
 				},
 			},
@@ -184,9 +189,10 @@ func JSONDataWithProperty(f *framework.Framework) (string, func(*framework.TestC
 func JSONDataWithoutTargetName(f *framework.Framework) (string, func(*framework.TestCase)) {
 	return "[common] should sync with empty target name, using json.", func(tc *framework.TestCase) {
 		secretKey := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
+		remoteRefKey := f.MakeRemoteRefKey(secretKey)
 		secretValue := "{\"foo\":\"foo-val\",\"bar\":\"bar-val\"}"
 		tc.Secrets = map[string]framework.SecretEntry{
-			secretKey: {Value: secretValue},
+			remoteRefKey: {Value: secretValue},
 		}
 		tc.ExpectedSecret = &v1.Secret{
 			Type: v1.SecretTypeOpaque,
@@ -199,7 +205,7 @@ func JSONDataWithoutTargetName(f *framework.Framework) (string, func(*framework.
 			{
 				SecretKey: secretKey,
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key:      secretKey,
+					Key:      remoteRefKey,
 					Property: "foo",
 				},
 			},
@@ -213,9 +219,11 @@ func JSONDataWithTemplate(f *framework.Framework) (string, func(*framework.TestC
 	return "[common] should sync json secrets with template", func(tc *framework.TestCase) {
 		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
 		secretKey2 := fmt.Sprintf("%s-%s", f.Namespace.Name, "other")
+		remoteRefKey1 := f.MakeRemoteRefKey(secretKey1)
+		remoteRefKey2 := f.MakeRemoteRefKey(secretKey2)
 		tc.Secrets = map[string]framework.SecretEntry{
-			secretKey1: {Value: secretValue1},
-			secretKey2: {Value: secretValue2},
+			remoteRefKey1: {Value: secretValue1},
+			remoteRefKey2: {Value: secretValue2},
 		}
 		tc.ExpectedSecret = &v1.Secret{
 			Type: v1.SecretTypeOpaque,
@@ -248,14 +256,14 @@ func JSONDataWithTemplate(f *framework.Framework) (string, func(*framework.TestC
 			{
 				SecretKey: "one",
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key:      secretKey1,
+					Key:      remoteRefKey1,
 					Property: "foo1",
 				},
 			},
 			{
 				SecretKey: "two",
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key:      secretKey2,
+					Key:      remoteRefKey2,
 					Property: "bar2",
 				},
 			},
@@ -269,9 +277,11 @@ func JSONDataWithTemplateFromLiteral(f *framework.Framework) (string, func(*fram
 	return "[common] should sync json secrets with template", func(tc *framework.TestCase) {
 		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
 		secretKey2 := fmt.Sprintf("%s-%s", f.Namespace.Name, "other")
+		remoteRefKey1 := f.MakeRemoteRefKey(secretKey1)
+		remoteRefKey2 := f.MakeRemoteRefKey(secretKey2)
 		tc.Secrets = map[string]framework.SecretEntry{
-			secretKey1: {Value: secretValue1},
-			secretKey2: {Value: secretValue2},
+			remoteRefKey1: {Value: secretValue1},
+			remoteRefKey2: {Value: secretValue2},
 		}
 		tc.ExpectedSecret = &v1.Secret{
 			Type: v1.SecretTypeOpaque,
@@ -292,14 +302,14 @@ func JSONDataWithTemplateFromLiteral(f *framework.Framework) (string, func(*fram
 			{
 				SecretKey: "one",
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key:      secretKey1,
+					Key:      remoteRefKey1,
 					Property: "foo1",
 				},
 			},
 			{
 				SecretKey: "two",
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key:      secretKey2,
+					Key:      remoteRefKey2,
 					Property: "bar2",
 				},
 			},
@@ -313,6 +323,8 @@ func TemplateFromConfigmaps(f *framework.Framework) (string, func(*framework.Tes
 	return "[common] should sync from templateFrom Configmaps", func(tc *framework.TestCase) {
 		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
 		secretKey2 := fmt.Sprintf("%s-%s", f.Namespace.Name, "other")
+		remoteRefKey1 := f.MakeRemoteRefKey(secretKey1)
+		remoteRefKey2 := f.MakeRemoteRefKey(secretKey2)
 		tc.AdditionalObjects = []client.Object{
 			&v1.ConfigMap{
 				ObjectMeta: metav1.ObjectMeta{
@@ -326,8 +338,8 @@ func TemplateFromConfigmaps(f *framework.Framework) (string, func(*framework.Tes
 			},
 		}
 		tc.Secrets = map[string]framework.SecretEntry{
-			secretKey1: {Value: secretValue1},
-			secretKey2: {Value: secretValue2},
+			remoteRefKey1: {Value: secretValue1},
+			remoteRefKey2: {Value: secretValue2},
 		}
 		tc.ExpectedSecret = &v1.Secret{
 			Type: v1.SecretTypeOpaque,
@@ -360,14 +372,14 @@ func TemplateFromConfigmaps(f *framework.Framework) (string, func(*framework.Tes
 			{
 				SecretKey: "one",
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key:      secretKey1,
+					Key:      remoteRefKey1,
 					Property: "foo1",
 				},
 			},
 			{
 				SecretKey: "two",
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key:      secretKey2,
+					Key:      remoteRefKey2,
 					Property: "bar2",
 				},
 			},
@@ -379,13 +391,14 @@ func TemplateFromConfigmaps(f *framework.Framework) (string, func(*framework.Tes
 func JSONDataFromSync(f *framework.Framework) (string, func(*framework.TestCase)) {
 	return "[common] should sync secrets with dataFrom", func(tc *framework.TestCase) {
 		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
+		remoteRefKey1 := f.MakeRemoteRefKey(secretKey1)
 		targetSecretKey1 := "name"
 		targetSecretValue1 := "great-name"
 		targetSecretKey2 := "surname"
 		targetSecretValue2 := "great-surname"
 		secretValue := fmt.Sprintf("{ %q: %q, %q: %q }", targetSecretKey1, targetSecretValue1, targetSecretKey2, targetSecretValue2)
 		tc.Secrets = map[string]framework.SecretEntry{
-			secretKey1: {Value: secretValue},
+			remoteRefKey1: {Value: secretValue},
 		}
 		tc.ExpectedSecret = &v1.Secret{
 			Type: v1.SecretTypeOpaque,
@@ -397,7 +410,7 @@ func JSONDataFromSync(f *framework.Framework) (string, func(*framework.TestCase)
 		tc.ExternalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
 			{
 				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
-					Key: secretKey1,
+					Key: remoteRefKey1,
 				},
 			},
 		}
@@ -408,13 +421,14 @@ func JSONDataFromSync(f *framework.Framework) (string, func(*framework.TestCase)
 func JSONDataFromRewrite(f *framework.Framework) (string, func(*framework.TestCase)) {
 	return "[common] should sync and rewrite secrets with dataFrom", func(tc *framework.TestCase) {
 		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
+		remoteRefKey1 := f.MakeRemoteRefKey(secretKey1)
 		targetSecretKey1 := "username"
 		targetSecretValue1 := "myuser.name"
 		targetSecretKey2 := "address"
 		targetSecretValue2 := "happy street"
 		secretValue := fmt.Sprintf("{ %q: %q, %q: %q }", targetSecretKey1, targetSecretValue1, targetSecretKey2, targetSecretValue2)
 		tc.Secrets = map[string]framework.SecretEntry{
-			secretKey1: {Value: secretValue},
+			remoteRefKey1: {Value: secretValue},
 		}
 		tc.ExpectedSecret = &v1.Secret{
 			Type: v1.SecretTypeOpaque,
@@ -426,7 +440,7 @@ func JSONDataFromRewrite(f *framework.Framework) (string, func(*framework.TestCa
 		tc.ExternalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
 			{
 				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
-					Key: secretKey1,
+					Key: remoteRefKey1,
 				},
 				Rewrite: []esv1beta1.ExternalSecretRewrite{
 					{
@@ -447,6 +461,7 @@ func JSONDataFromRewrite(f *framework.Framework) (string, func(*framework.TestCa
 func NestedJSONWithGJSON(f *framework.Framework) (string, func(*framework.TestCase)) {
 	return "[common] should sync nested json secrets and get inner keys", func(tc *framework.TestCase) {
 		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
+		remoteRefKey1 := f.MakeRemoteRefKey(secretKey1)
 		targetSecretKey1 := "firstname"
 		targetSecretValue1 := "Tom"
 		targetSecretKey2 := "first_friend"
@@ -462,7 +477,7 @@ func NestedJSONWithGJSON(f *framework.Framework) (string, func(*framework.TestCa
 				]
 			}`, targetSecretValue1, targetSecretValue2)
 		tc.Secrets = map[string]framework.SecretEntry{
-			secretKey1: {Value: secretValue},
+			remoteRefKey1: {Value: secretValue},
 		}
 		tc.ExpectedSecret = &v1.Secret{
 			Type: v1.SecretTypeOpaque,
@@ -475,14 +490,14 @@ func NestedJSONWithGJSON(f *framework.Framework) (string, func(*framework.TestCa
 			{
 				SecretKey: targetSecretKey1,
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key:      secretKey1,
+					Key:      remoteRefKey1,
 					Property: "name.first",
 				},
 			},
 			{
 				SecretKey: targetSecretKey2,
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key:      secretKey1,
+					Key:      remoteRefKey1,
 					Property: "friends.1.first",
 				},
 			},
@@ -496,10 +511,11 @@ func NestedJSONWithGJSON(f *framework.Framework) (string, func(*framework.TestCa
 func DockerJSONConfig(f *framework.Framework) (string, func(*framework.TestCase)) {
 	return "[common] should sync docker configurated json secrets with template simple", func(tc *framework.TestCase) {
 		cloudSecretName := fmt.Sprintf("%s-%s", f.Namespace.Name, dockerConfigExampleName)
+		cloudRemoteRefKey := f.MakeRemoteRefKey(cloudSecretName)
 		dockerconfig := `{"auths":{"https://index.docker.io/v1/": {"auth": "c3R...zE2"}}}`
 		cloudSecretValue := fmt.Sprintf(`{"dockerconfig": %s}`, dockerconfig)
 		tc.Secrets = map[string]framework.SecretEntry{
-			cloudSecretName: {Value: cloudSecretValue},
+			cloudRemoteRefKey: {Value: cloudSecretValue},
 		}
 
 		tc.ExpectedSecret = &v1.Secret{
@@ -513,7 +529,7 @@ func DockerJSONConfig(f *framework.Framework) (string, func(*framework.TestCase)
 			{
 				SecretKey: "mysecret",
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key:      cloudSecretName,
+					Key:      cloudRemoteRefKey,
 					Property: "dockerconfig",
 				},
 			},
@@ -533,11 +549,12 @@ func DockerJSONConfig(f *framework.Framework) (string, func(*framework.TestCase)
 func DataPropertyDockerconfigJSON(f *framework.Framework) (string, func(*framework.TestCase)) {
 	return "[common] should sync docker configurated json secrets with template", func(tc *framework.TestCase) {
 		cloudSecretName := fmt.Sprintf("%s-%s", f.Namespace.Name, dockerConfigExampleName)
+		cloudRemoteRefKey := f.MakeRemoteRefKey(cloudSecretName)
 		dockerconfigString := `"{\"auths\":{\"https://index.docker.io/v1/\": {\"auth\": \"c3R...zE2\"}}}"`
 		dockerconfig := `{"auths":{"https://index.docker.io/v1/": {"auth": "c3R...zE2"}}}`
 		cloudSecretValue := fmt.Sprintf(`{"dockerconfig": %s}`, dockerconfigString)
 		tc.Secrets = map[string]framework.SecretEntry{
-			cloudSecretName: {Value: cloudSecretValue},
+			cloudRemoteRefKey: {Value: cloudSecretValue},
 		}
 
 		tc.ExpectedSecret = &v1.Secret{
@@ -551,7 +568,7 @@ func DataPropertyDockerconfigJSON(f *framework.Framework) (string, func(*framewo
 			{
 				SecretKey: "mysecret",
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key:      cloudSecretName,
+					Key:      cloudRemoteRefKey,
 					Property: "dockerconfig",
 				},
 			},
@@ -609,9 +626,10 @@ func SSHKeySync(f *framework.Framework) (string, func(*framework.TestCase)) {
 		PKDc8xGEXdd4A6jnwJBifJs+UpPrHAh0c63KfjO3rryDycvmxeWRnyU1yRCUjIuH31vi+L
 		OkcGfqTaOoz2KVAAAAFGtpYW5AREVTS1RPUC1TNFI5S1JQAQIDBAUG
 		-----END OPENSSH PRIVATE KEY-----`
+		sshRemoteRefKey := f.MakeRemoteRefKey(sshSecretName)
 
 		tc.Secrets = map[string]framework.SecretEntry{
-			sshSecretName: {Value: sshSecretValue},
+			sshRemoteRefKey: {Value: sshSecretValue},
 		}
 
 		tc.ExpectedSecret = &v1.Secret{
@@ -625,7 +643,7 @@ func SSHKeySync(f *framework.Framework) (string, func(*framework.TestCase)) {
 			{
 				SecretKey: "mysecret",
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key: sshSecretName,
+					Key: sshRemoteRefKey,
 				},
 			},
 		}
@@ -643,6 +661,7 @@ func SSHKeySync(f *framework.Framework) (string, func(*framework.TestCase)) {
 func SSHKeySyncDataProperty(f *framework.Framework) (string, func(*framework.TestCase)) {
 	return "[common] should sync ssh key with provider.", func(tc *framework.TestCase) {
 		cloudSecretName := fmt.Sprintf("%s-%s", f.Namespace.Name, dockerConfigExampleName)
+		cloudRemoteRefKey := f.MakeRemoteRefKey(cloudSecretName)
 		SSHKey := `-----BEGIN OPENSSH PRIVATE KEY-----
 		b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
 		NhAAAAAwEAAQAAAYEAsARoZUqo6L5dd0WRjZ2QPq/kKlbjtUY1njzJ01UtdC1u1eSJFUnV
@@ -683,7 +702,7 @@ func SSHKeySyncDataProperty(f *framework.Framework) (string, func(*framework.Tes
 		-----END OPENSSH PRIVATE KEY-----`
 		cloudSecretValue := fmt.Sprintf(`{"ssh-auth": %q}`, SSHKey)
 		tc.Secrets = map[string]framework.SecretEntry{
-			cloudSecretName: {Value: cloudSecretValue},
+			cloudRemoteRefKey: {Value: cloudSecretValue},
 		}
 
 		tc.ExpectedSecret = &v1.Secret{
@@ -697,7 +716,7 @@ func SSHKeySyncDataProperty(f *framework.Framework) (string, func(*framework.Tes
 			{
 				SecretKey: "mysecret",
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key:      cloudSecretName,
+					Key:      cloudRemoteRefKey,
 					Property: "ssh-auth",
 				},
 			},
@@ -716,10 +735,12 @@ func DeletionPolicyDelete(f *framework.Framework) (string, func(*framework.TestC
 	return "[common] should delete secret when provider secret was deleted using .data[]", func(tc *framework.TestCase) {
 		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
 		secretKey2 := fmt.Sprintf("%s-%s", f.Namespace.Name, "other")
+		remoteRefKey1 := f.MakeRemoteRefKey(secretKey1)
+		remoteRefKey2 := f.MakeRemoteRefKey(secretKey2)
 		secretValue := "bazz"
 		tc.Secrets = map[string]framework.SecretEntry{
-			secretKey1: {Value: secretValue},
-			secretKey2: {Value: secretValue},
+			remoteRefKey1: {Value: secretValue},
+			remoteRefKey2: {Value: secretValue},
 		}
 		tc.ExpectedSecret = &v1.Secret{
 			Type: v1.SecretTypeOpaque,
@@ -734,19 +755,19 @@ func DeletionPolicyDelete(f *framework.Framework) (string, func(*framework.TestC
 			{
 				SecretKey: secretKey1,
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key: secretKey1,
+					Key: remoteRefKey1,
 				},
 			},
 			{
 				SecretKey: secretKey2,
 				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
-					Key: secretKey2,
+					Key: remoteRefKey2,
 				},
 			},
 		}
 		tc.AfterSync = func(prov framework.SecretStoreProvider, secret *v1.Secret) {
-			prov.DeleteSecret(secretKey1)
-			prov.DeleteSecret(secretKey2)
+			prov.DeleteSecret(remoteRefKey1)
+			prov.DeleteSecret(remoteRefKey2)
 
 			gomega.Eventually(func() bool {
 				_, err := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Get(context.Background(), secret.Name, metav1.GetOptions{})

+ 6 - 6
e2e/suites/provider/cases/common/find_by_name.go

@@ -34,9 +34,9 @@ func FindByName(f *framework.Framework) (string, func(*framework.TestCase)) {
 		secretKeyThree := fmt.Sprintf(namePrefix, f.Namespace.Name, "three")
 		secretValue := findValue
 		tc.Secrets = map[string]framework.SecretEntry{
-			secretKeyOne:   {Value: secretValue},
-			secretKeyTwo:   {Value: secretValue},
-			secretKeyThree: {Value: secretValue},
+			f.MakeRemoteRefKey(secretKeyOne):   {Value: secretValue},
+			f.MakeRemoteRefKey(secretKeyTwo):   {Value: secretValue},
+			f.MakeRemoteRefKey(secretKeyThree): {Value: secretValue},
 		}
 		tc.ExpectedSecret = &v1.Secret{
 			Type: v1.SecretTypeOpaque,
@@ -70,9 +70,9 @@ func FindByNameAndRewrite(f *framework.Framework) (string, func(*framework.TestC
 		expectedKeyThree := fmt.Sprintf("%s_%s", f.Namespace.Name, "three")
 		secretValue := findValue
 		tc.Secrets = map[string]framework.SecretEntry{
-			secretKeyOne:   {Value: secretValue},
-			secretKeyTwo:   {Value: secretValue},
-			secretKeyThree: {Value: secretValue},
+			f.MakeRemoteRefKey(secretKeyOne):   {Value: secretValue},
+			f.MakeRemoteRefKey(secretKeyTwo):   {Value: secretValue},
+			f.MakeRemoteRefKey(secretKeyThree): {Value: secretValue},
 		}
 		tc.ExpectedSecret = &v1.Secret{
 			Type: v1.SecretTypeOpaque,

+ 3 - 3
e2e/suites/provider/cases/common/find_by_tags.go

@@ -29,17 +29,17 @@ func FindByTag(f *framework.Framework) (string, func(*framework.TestCase)) {
 		secretKeyTwo := fmt.Sprintf(namePrefix, f.Namespace.Name, "two")
 		secretKeyThree := fmt.Sprintf(namePrefix, f.Namespace.Name, "three")
 		tc.Secrets = map[string]framework.SecretEntry{
-			secretKeyOne: {
+			f.MakeRemoteRefKey(secretKeyOne): {
 				Value: secretValue1,
 				Tags: map[string]string{
 					"test": f.Namespace.Name,
 				}},
-			secretKeyTwo: {
+			f.MakeRemoteRefKey(secretKeyTwo): {
 				Value: secretValue1,
 				Tags: map[string]string{
 					"test": f.Namespace.Name,
 				}},
-			secretKeyThree: {
+			f.MakeRemoteRefKey(secretKeyThree): {
 				Value: secretValue1,
 				Tags: map[string]string{
 					"test": f.Namespace.Name,

+ 1 - 0
e2e/suites/provider/cases/import.go

@@ -21,6 +21,7 @@ import (
 	_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/azure"
 	_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/gcp"
 	_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/kubernetes"
+	_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/scaleway"
 	_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/template"
 	_ "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/vault"
 )

+ 56 - 0
e2e/suites/provider/cases/scaleway/config.go

@@ -0,0 +1,56 @@
+package scaleway
+
+import (
+	"fmt"
+	"os"
+)
+
+type config struct {
+	apiUrl    *string
+	region    string
+	projectId string
+	accessKey string
+	secretKey string
+}
+
+func loadConfigFromEnv() (*config, error) {
+
+	var cfg config
+	var err error
+
+	if apiUrl, ok := os.LookupEnv("SCALEWAY_API_URL"); ok {
+		cfg.apiUrl = &apiUrl
+	}
+
+	cfg.region, err = getEnv("SCALEWAY_REGION")
+	if err != nil {
+		return nil, err
+	}
+
+	cfg.projectId, err = getEnv("SCALEWAY_PROJECT_ID")
+	if err != nil {
+		return nil, err
+	}
+
+	cfg.accessKey, err = getEnv("SCALEWAY_ACCESS_KEY")
+	if err != nil {
+		return nil, err
+	}
+
+	cfg.secretKey, err = getEnv("SCALEWAY_SECRET_KEY")
+	if err != nil {
+		return nil, err
+	}
+
+	return &cfg, nil
+}
+
+func getEnv(name string) (string, error) {
+
+	value, ok := os.LookupEnv(name)
+	if !ok {
+		return "", fmt.Errorf("environment variable %q is not set", name)
+	}
+
+	return value, nil
+}

+ 106 - 0
e2e/suites/provider/cases/scaleway/provider.go

@@ -0,0 +1,106 @@
+package scaleway
+
+import (
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	"github.com/onsi/gomega"
+	smapi "github.com/scaleway/scaleway-sdk-go/api/secret/v1alpha1"
+	"github.com/scaleway/scaleway-sdk-go/scw"
+)
+
+const remoteRefPrefix = "name:"
+const cleanupTag = "eso-e2e" // tag for easy cleanup
+
+type secretStoreProvider struct {
+	api *smapi.API
+	cfg *config
+}
+
+func (p *secretStoreProvider) init(cfg *config) {
+
+	p.cfg = cfg
+
+	options := []scw.ClientOption{
+		scw.WithDefaultRegion(scw.Region(cfg.region)),
+		scw.WithDefaultProjectID(cfg.projectId),
+		scw.WithAuth(cfg.accessKey, cfg.secretKey),
+	}
+
+	if cfg.apiUrl != nil {
+		options = append(options, scw.WithAPIURL(*cfg.apiUrl))
+	}
+
+	scwClient, err := scw.NewClient(options...)
+	gomega.Expect(err).ToNot(gomega.HaveOccurred())
+
+	p.api = smapi.NewAPI(scwClient)
+}
+
+// cleanup prevents accumulation of secrets after aborted runs.
+func (p *secretStoreProvider) cleanup() {
+
+	for {
+		listResp, err := p.api.ListSecrets(&smapi.ListSecretsRequest{
+			ProjectID: &p.cfg.projectId,
+			Tags:      []string{cleanupTag},
+		})
+		gomega.Expect(err).ToNot(gomega.HaveOccurred())
+
+		for _, secret := range listResp.Secrets {
+			err := p.api.DeleteSecret(&smapi.DeleteSecretRequest{
+				SecretID: secret.ID,
+			})
+			gomega.Expect(err).ToNot(gomega.HaveOccurred())
+		}
+
+		if uint32(len(listResp.Secrets)) == listResp.TotalCount {
+			break
+		}
+	}
+}
+
+func (p *secretStoreProvider) CreateSecret(key string, val framework.SecretEntry) {
+
+	gomega.Expect(key).To(gomega.HavePrefix(remoteRefPrefix))
+	secretName := key[len(remoteRefPrefix):]
+
+	var tags []string
+	for tag := range val.Tags {
+		tags = append(tags, tag)
+	}
+
+	tags = append(tags, cleanupTag)
+
+	secret, err := p.api.CreateSecret(&smapi.CreateSecretRequest{
+		Name: secretName,
+		Tags: tags,
+	})
+	gomega.Expect(err).ToNot(gomega.HaveOccurred())
+
+	_, err = p.api.CreateSecretVersion(&smapi.CreateSecretVersionRequest{
+		SecretID: secret.ID,
+		Data:     []byte(val.Value),
+	})
+	gomega.Expect(err).ToNot(gomega.HaveOccurred())
+}
+
+func (p *secretStoreProvider) DeleteSecret(key string) {
+
+	gomega.Expect(key).To(gomega.HavePrefix(remoteRefPrefix))
+	secretName := key[len(remoteRefPrefix):]
+
+	secret, err := p.api.GetSecretByName(&smapi.GetSecretByNameRequest{
+		SecretName: secretName,
+	})
+	if _, isErrNotFound := err.(*scw.ResourceNotFoundError); isErrNotFound {
+		return
+	}
+	gomega.Expect(err).ToNot(gomega.HaveOccurred())
+
+	err = p.api.DeleteSecret(&smapi.DeleteSecretRequest{
+		SecretID: secret.ID,
+	})
+	if _, isErrNotFound := err.(*scw.ResourceNotFoundError); isErrNotFound {
+		return
+	}
+	gomega.Expect(err).ToNot(gomega.HaveOccurred())
+}

+ 121 - 0
e2e/suites/provider/cases/scaleway/scaleway.go

@@ -0,0 +1,121 @@
+package scaleway
+
+import (
+	"context"
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/onsi/ginkgo/v2"
+	"github.com/onsi/gomega"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sync"
+)
+
+var cleanupOnce sync.Once
+
+var _ = ginkgo.Describe("[scaleway]", ginkgo.Label("scaleway"), func() {
+
+	f := framework.New("eso-scaleway")
+	f.MakeRemoteRefKey = func(base string) string {
+		return "name:" + base
+	}
+
+	// Initialization is deferred so that assertions work.
+	provider := &secretStoreProvider{}
+
+	ginkgo.BeforeEach(func() {
+
+		cfg, err := loadConfigFromEnv()
+		gomega.Expect(err).ToNot(gomega.HaveOccurred())
+
+		provider.init(cfg)
+
+		cleanupOnce.Do(provider.cleanup)
+
+		createResources(context.Background(), f, cfg)
+	})
+
+	ginkgo.DescribeTable("sync secrets", framework.TableFunc(f, provider),
+
+		//ginkgo.Entry(common.SyncV1Alpha1(f)), // not supported
+		ginkgo.Entry(common.SimpleDataSync(f)),
+		ginkgo.Entry(common.SyncWithoutTargetName(f)),
+		ginkgo.Entry(common.JSONDataWithProperty(f)),
+		ginkgo.Entry(common.JSONDataWithoutTargetName(f)),
+		ginkgo.Entry(common.JSONDataWithTemplate(f)),
+		ginkgo.Entry(common.JSONDataWithTemplateFromLiteral(f)),
+		ginkgo.Entry(common.TemplateFromConfigmaps(f)),
+		ginkgo.Entry(common.JSONDataFromSync(f)),
+		ginkgo.Entry(common.JSONDataFromRewrite(f)),
+		ginkgo.Entry(common.NestedJSONWithGJSON(f)),
+		ginkgo.Entry(common.DockerJSONConfig(f)),
+		ginkgo.Entry(common.DataPropertyDockerconfigJSON(f)),
+		ginkgo.Entry(common.SSHKeySync(f)),
+		ginkgo.Entry(common.SSHKeySyncDataProperty(f)),
+		ginkgo.Entry(common.DeletionPolicyDelete(f)),
+		//ginkgo.Entry(common.DecodingPolicySync(f)), // not supported
+
+		ginkgo.Entry(common.FindByName(f)),
+		ginkgo.Entry(common.FindByNameAndRewrite(f)),
+		//ginkgo.Entry(common.FindByNameWithPath(f)), // not supported
+
+		ginkgo.Entry(common.FindByTag(f)),
+		//ginkgo.Entry(common.FindByTagWithPath(f)), // not supported
+	)
+})
+
+func createResources(ctx context.Context, f *framework.Framework, cfg *config) {
+
+	apiKeySecretName := "scw-api-key"
+	apiKeySecretKey := "secret-key"
+
+	// Creating a secret to hold the API key.
+
+	secretSpec := v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      apiKeySecretName,
+			Namespace: f.Namespace.Name,
+		},
+		StringData: map[string]string{
+			"secret-key": cfg.secretKey,
+		},
+	}
+
+	err := f.CRClient.Create(ctx, &secretSpec)
+	gomega.Expect(err).ToNot(gomega.HaveOccurred())
+
+	// Creating SecretStore.
+
+	secretStoreSpec := esv1beta1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      f.Namespace.Name,
+			Namespace: f.Namespace.Name,
+		},
+		Spec: esv1beta1.SecretStoreSpec{
+			Provider: &esv1beta1.SecretStoreProvider{
+				Scaleway: &esv1beta1.ScalewayProvider{
+					Region:    cfg.region,
+					ProjectID: cfg.projectId,
+					AccessKey: &esv1beta1.ScalewayProviderSecretRef{
+						Value: cfg.accessKey, // TODO: test with secretRef as well
+					},
+					SecretKey: &esv1beta1.ScalewayProviderSecretRef{
+						SecretRef: &esmeta.SecretKeySelector{
+							Name: apiKeySecretName,
+							Key:  apiKeySecretKey,
+						},
+					},
+				},
+			},
+		},
+	}
+
+	if cfg.apiUrl != nil {
+		secretStoreSpec.Spec.Provider.Scaleway.APIURL = *cfg.apiUrl
+	}
+
+	err = f.CRClient.Create(ctx, &secretStoreSpec)
+	gomega.Expect(err).ToNot(gomega.HaveOccurred())
+}

+ 1 - 0
go.mod

@@ -68,6 +68,7 @@ require (
 	github.com/hashicorp/golang-lru v0.5.4
 	github.com/keeper-security/secrets-manager-go/core v1.5.0
 	github.com/maxbrunsfeld/counterfeiter/v6 v6.6.1
+	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.13.0.20230227165516-144b3e06ecf2
 	github.com/sethvargo/go-password v0.2.0
 	github.com/spf13/pflag v1.0.5
 	sigs.k8s.io/yaml v1.3.0

+ 3 - 1
go.sum

@@ -157,7 +157,7 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
 github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
 github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
-github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c=
+github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE=
 github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
@@ -507,6 +507,8 @@ github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2u
 github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
 github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.13.0.20230227165516-144b3e06ecf2 h1:LyHBuaec79FY+tmMHUdnctd/es78NzZ7VKcE2QmdPTc=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.13.0.20230227165516-144b3e06ecf2/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
 github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
 github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
 github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=

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

@@ -98,6 +98,7 @@ nav:
     - senhasegura DevOps Secrets Management (DSM): provider/senhasegura-dsm.md
     - Doppler: provider/doppler.md
     - Keeper Security: provider/keeper-security.md
+    - Scaleway: provider/scaleway.md
   - Examples:
     - FluxCD: examples/gitops-using-fluxcd.md
     - Anchore Engine: examples/anchore-engine-credentials.md

+ 1 - 0
pkg/generator/register/register.go

@@ -16,6 +16,7 @@ limitations under the License.
 package register
 
 // packages imported here are registered to the controller schema.
+
 import (
 	_ "github.com/external-secrets/external-secrets/pkg/generator/acr"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/ecr"

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

@@ -16,6 +16,7 @@ limitations under the License.
 package register
 
 // packages imported here are registered to the controller schema.
+
 import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/akeyless"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/alibaba"
@@ -30,6 +31,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/kubernetes"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/onepassword"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/oracle"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/scaleway"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/senhasegura"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/vault"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/webhook"

+ 97 - 0
pkg/provider/scaleway/cache.go

@@ -0,0 +1,97 @@
+/*
+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 scaleway
+
+import (
+	"container/list"
+	"fmt"
+	"sync"
+)
+
+// cache is for caching values of the secrets. Secret versions are immutable, thus there is no need
+// for time-based expiration.
+type cache interface {
+	Get(secretID string, revision uint32) ([]byte, bool)
+	Put(secretID string, revision uint32, value []byte)
+}
+
+type cacheEntry struct {
+	value []byte
+	elem  *list.Element
+}
+
+type cacheImpl struct {
+	mutex                sync.Mutex
+	entries              map[string]cacheEntry
+	entryKeysByLastUsage list.List
+	maxEntryCount        int
+}
+
+func newCache() cache {
+	return &cacheImpl{
+		entries:       map[string]cacheEntry{},
+		maxEntryCount: 500,
+	}
+}
+
+func (c *cacheImpl) Get(secretID string, revision uint32) ([]byte, bool) {
+	c.mutex.Lock()
+	defer c.mutex.Unlock()
+
+	key := c.key(secretID, revision)
+
+	entry, ok := c.entries[key]
+	if !ok {
+		return nil, false
+	}
+
+	c.entryKeysByLastUsage.MoveToFront(entry.elem)
+
+	return entry.value, true
+}
+
+func (c *cacheImpl) Put(secretID string, revision uint32, value []byte) {
+	c.mutex.Lock()
+	defer c.mutex.Unlock()
+
+	key := c.key(secretID, revision)
+
+	_, alreadyPresent := c.entries[key]
+	if alreadyPresent {
+		return
+	}
+
+	if len(c.entries) == c.maxEntryCount {
+		c.evictLeastRecentlyUsed()
+	}
+
+	entry := c.entryKeysByLastUsage.PushFront(key)
+
+	c.entries[key] = cacheEntry{
+		value: value,
+		elem:  entry,
+	}
+}
+
+func (c *cacheImpl) evictLeastRecentlyUsed() {
+	elem := c.entryKeysByLastUsage.Back()
+
+	delete(c.entries, elem.Value.(string))
+
+	c.entryKeysByLastUsage.Remove(elem)
+}
+
+func (c *cacheImpl) key(secretID string, revision uint32) string {
+	return fmt.Sprintf("%s/%d", secretID, revision)
+}

+ 63 - 0
pkg/provider/scaleway/cache_test.go

@@ -0,0 +1,63 @@
+/*
+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 scaleway
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCacheMissReturnsFalse(t *testing.T) {
+	cache := newCache()
+
+	_, ok := cache.Get("26f72b22-bcae-4131-a26e-b98abb3fa3dd", 1)
+
+	assert.False(t, ok)
+}
+
+func TestCachePutThenGet(t *testing.T) {
+	cache := newCache()
+	secretID := "cfd5dda5-dedb-40eb-b9c4-b9cf8e254727"
+	revision := uint32(1)
+	expectedValue := []byte("some value")
+
+	cache.Put(secretID, revision, expectedValue)
+
+	value, ok := cache.Get(secretID, revision)
+	assert.True(t, ok)
+	assert.Equal(t, expectedValue, value)
+}
+
+func TestCacheLeastRecentlyUsedIsRemovedFirst(t *testing.T) {
+	cache := newCache()
+	secretID := "0c82ecf4-d3f7-4960-8301-0def5230eee2"
+	maxEntryCount := 500
+
+	for i := 0; i < maxEntryCount; i++ {
+		cache.Put(secretID, uint32(i+1), []byte{})
+	}
+
+	for i := 0; i < maxEntryCount; i++ {
+		cache.Get(secretID, uint32(i+1))
+	}
+
+	cache.Put(secretID, uint32(maxEntryCount+2), []byte{})
+
+	_, ok := cache.Get(secretID, 1)
+	assert.False(t, ok)
+
+	_, ok = cache.Get(secretID, uint32(maxEntryCount+2))
+	assert.True(t, ok)
+}

+ 444 - 0
pkg/provider/scaleway/client.go

@@ -0,0 +1,444 @@
+/*
+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 scaleway
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	smapi "github.com/scaleway/scaleway-sdk-go/api/secret/v1alpha1"
+	"github.com/scaleway/scaleway-sdk-go/scw"
+	"github.com/tidwall/gjson"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/find"
+)
+
+var errNoSecretForName = errors.New("no secret for this name")
+
+type client struct {
+	api       secretAPI
+	projectID string
+	cache     cache
+}
+
+const (
+	refTypeName = "name"
+	refTypeID   = "id"
+)
+
+type scwSecretRef struct {
+	RefType string
+	Value   string
+}
+
+func (r scwSecretRef) String() string {
+	return fmt.Sprintf("%s:%s", r.RefType, r.Value)
+}
+
+func decodeScwSecretRef(key string) (*scwSecretRef, error) {
+	sepIndex := strings.IndexRune(key, ':')
+	if sepIndex < 0 {
+		return nil, fmt.Errorf("invalid secret reference: missing colon ':'")
+	}
+
+	return &scwSecretRef{
+		RefType: key[:sepIndex],
+		Value:   key[sepIndex+1:],
+	}, nil
+}
+
+func (c *client) getSecretByName(ctx context.Context, name string) (*smapi.Secret, error) {
+	request := smapi.GetSecretByNameRequest{
+		SecretName: name,
+	}
+
+	response, err := c.api.GetSecretByName(&request, scw.WithContext(ctx))
+	if err != nil {
+		//nolint:errorlint
+		if _, isErrNotFound := err.(*scw.ResourceNotFoundError); isErrNotFound {
+			return nil, errNoSecretForName
+		}
+		return nil, err
+	}
+
+	return response, nil
+}
+
+func (c *client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	scwRef, err := decodeScwSecretRef(ref.Key)
+	if err != nil {
+		return nil, err
+	}
+
+	versionSpec := "latest_enabled"
+	if ref.Version != "" {
+		versionSpec = ref.Version
+	}
+
+	value, err := c.accessSecretVersion(ctx, scwRef, versionSpec)
+	if err != nil {
+		//nolint:errorlint
+		if _, isNotFoundErr := err.(*scw.ResourceNotFoundError); isNotFoundErr {
+			return nil, esv1beta1.NoSecretError{}
+		}
+		return nil, err
+	}
+
+	if ref.Property != "" {
+		extracted, err := extractJSONProperty(value, ref.Property)
+		if err != nil {
+			return nil, err
+		}
+
+		value = extracted
+	}
+
+	return value, nil
+}
+
+func (c *client) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
+	scwRef, err := decodeScwSecretRef(remoteRef.GetRemoteKey())
+	if err != nil {
+		return err
+	}
+
+	if scwRef.RefType != refTypeName {
+		return fmt.Errorf("secrets can only be pushed by name")
+	}
+	secretName := scwRef.Value
+
+	// First, we do a GetSecretVersion() to resolve the secret id and the last revision number.
+
+	var secretID string
+	secretExists := false
+	existingSecretVersion := int64(-1)
+
+	secretVersion, err := c.api.GetSecretVersionByName(&smapi.GetSecretVersionByNameRequest{
+		SecretName: secretName,
+		Revision:   "latest",
+	}, scw.WithContext(ctx))
+	if err != nil {
+		//nolint:errorlint
+		if notFoundErr, ok := err.(*scw.ResourceNotFoundError); ok {
+			if notFoundErr.Resource == "secret_version" {
+				secretExists = true
+			}
+		} else {
+			return err
+		}
+	} else {
+		secretExists = true
+		existingSecretVersion = int64(secretVersion.Revision)
+	}
+
+	if secretExists {
+		if existingSecretVersion != -1 {
+			// If the secret exists, we can fetch its last value to see if we have any change to make.
+
+			secretID = secretVersion.SecretID
+
+			data, err := c.accessSpecificSecretVersion(ctx, secretID, secretVersion.Revision)
+			if err != nil {
+				return err
+			}
+
+			if bytes.Equal(data, value) {
+				// No change to push.
+				return nil
+			}
+		} else {
+			// If the secret exists but has no versions, we need an additional GetSecret() to resolve the secret id.
+			// This may happen if a push was interrupted.
+
+			secret, err := c.api.GetSecretByName(&smapi.GetSecretByNameRequest{
+				SecretName: secretName,
+			}, scw.WithContext(ctx))
+			if err != nil {
+				return err
+			}
+
+			secretID = secret.ID
+		}
+	} else {
+		// If the secret does not exist, we need to create it.
+
+		secret, err := c.api.CreateSecret(&smapi.CreateSecretRequest{
+			ProjectID: c.projectID,
+			Name:      secretName,
+		}, scw.WithContext(ctx))
+		if err != nil {
+			return err
+		}
+
+		secretID = secret.ID
+	}
+
+	// Finally, we push the new secret version.
+
+	createSecretVersionRequest := smapi.CreateSecretVersionRequest{
+		SecretID: secretID,
+		Data:     value,
+	}
+
+	createSecretVersionResponse, err := c.api.CreateSecretVersion(&createSecretVersionRequest, scw.WithContext(ctx))
+	if err != nil {
+		return err
+	}
+
+	c.cache.Put(secretID, createSecretVersionResponse.Revision, value)
+
+	if secretExists && existingSecretVersion != -1 {
+		_, err := c.api.DisableSecretVersion(&smapi.DisableSecretVersionRequest{
+			SecretID: secretID,
+			Revision: fmt.Sprintf("%d", existingSecretVersion),
+		})
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (c *client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
+	scwRef, err := decodeScwSecretRef(remoteRef.GetRemoteKey())
+	if err != nil {
+		return err
+	}
+
+	if scwRef.RefType != refTypeName {
+		return fmt.Errorf("secrets can only be pushed by name")
+	}
+	secretName := scwRef.Value
+
+	secret, err := c.getSecretByName(ctx, secretName)
+	if err != nil {
+		if errors.Is(err, errNoSecretForName) {
+			return nil
+		}
+		return err
+	}
+
+	request := smapi.DeleteSecretRequest{
+		SecretID: secret.ID,
+	}
+
+	err = c.api.DeleteSecret(&request, scw.WithContext(ctx))
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *client) Validate() (esv1beta1.ValidationResult, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+
+	page := int32(1)
+	pageSize := uint32(0)
+	_, err := c.api.ListSecrets(&smapi.ListSecretsRequest{
+		ProjectID: &c.projectID,
+		Page:      &page,
+		PageSize:  &pageSize,
+	}, scw.WithContext(ctx))
+	if err != nil {
+		return esv1beta1.ValidationResultError, nil
+	}
+
+	return esv1beta1.ValidationResultReady, nil
+}
+
+func (c *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	rawData, err := c.GetSecret(ctx, ref)
+	if err != nil {
+		return nil, err
+	}
+
+	structuredData := make(map[string]json.RawMessage)
+
+	err = json.Unmarshal(rawData, &structuredData)
+	if err != nil {
+		return nil, err
+	}
+
+	values := make(map[string][]byte)
+
+	for key, value := range structuredData {
+		values[key] = jsonToSecretData(value)
+	}
+
+	return values, nil
+}
+
+// GetAllSecrets lists secrets matching the given criteria and return their latest versions.
+func (c *client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	request := smapi.ListSecretsRequest{
+		ProjectID: &c.projectID,
+		Page:      new(int32),
+		PageSize:  new(uint32),
+	}
+	*request.Page = 1
+	*request.PageSize = 50
+
+	if ref.Path != nil {
+		return nil, fmt.Errorf("searching by path is not supported")
+	}
+
+	var nameMatcher *find.Matcher
+	if ref.Name != nil {
+		var err error
+		nameMatcher, err = find.New(*ref.Name)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	for tag := range ref.Tags {
+		request.Tags = append(request.Tags, tag)
+	}
+
+	results := map[string][]byte{}
+
+	for done := false; !done; {
+		response, err := c.api.ListSecrets(&request, scw.WithContext(ctx))
+		if err != nil {
+			return nil, err
+		}
+
+		totalFetched := uint64(*request.Page-1)*uint64(*request.PageSize) + uint64(len(response.Secrets))
+		done = totalFetched == uint64(response.TotalCount)
+
+		*request.Page++
+
+		for _, secret := range response.Secrets {
+			if nameMatcher != nil && !nameMatcher.MatchName(secret.Name) {
+				continue
+			}
+
+			accessReq := smapi.AccessSecretVersionRequest{
+				Region:   secret.Region,
+				SecretID: secret.ID,
+				Revision: "latest_enabled",
+			}
+
+			accessResp, err := c.api.AccessSecretVersion(&accessReq, scw.WithContext(ctx))
+			if err != nil {
+				log.Error(err, "failed to access secret")
+				continue
+			}
+
+			results[secret.Name] = accessResp.Data
+		}
+	}
+
+	return results, nil
+}
+
+func (c *client) Close(context.Context) error {
+	return nil
+}
+
+func (c *client) accessSecretVersion(ctx context.Context, secretRef *scwSecretRef, versionSpec string) ([]byte, error) {
+	// if we have a secret id and a revision number, we can avoid an extra GetSecret()
+
+	if secretRef.RefType == refTypeID && len(versionSpec) > 0 && '0' <= versionSpec[0] && versionSpec[0] <= '9' {
+		secretID := secretRef.Value
+
+		revision, err := strconv.ParseUint(versionSpec, 10, 32)
+		if err == nil {
+			return c.accessSpecificSecretVersion(ctx, secretID, uint32(revision))
+		}
+	}
+
+	// otherwise, we do a GetSecret() first to avoid transferring the secret value if it is cached
+
+	var secretID string
+	var secretRevision uint32
+
+	switch secretRef.RefType {
+	case refTypeID:
+		request := smapi.GetSecretVersionRequest{
+			SecretID: secretRef.Value,
+			Revision: versionSpec,
+		}
+		response, err := c.api.GetSecretVersion(&request, scw.WithContext(ctx))
+		if err != nil {
+			return nil, err
+		}
+		secretID = response.SecretID
+		secretRevision = response.Revision
+	case refTypeName:
+		request := smapi.GetSecretVersionByNameRequest{
+			SecretName: secretRef.Value,
+			Revision:   versionSpec,
+		}
+		response, err := c.api.GetSecretVersionByName(&request, scw.WithContext(ctx))
+		if err != nil {
+			return nil, err
+		}
+		secretID = response.SecretID
+		secretRevision = response.Revision
+	default:
+		return nil, fmt.Errorf("invalid secret reference: %q", secretRef.Value)
+	}
+
+	return c.accessSpecificSecretVersion(ctx, secretID, secretRevision)
+}
+
+func (c *client) accessSpecificSecretVersion(ctx context.Context, secretID string, revision uint32) ([]byte, error) {
+	cachedValue, cacheHit := c.cache.Get(secretID, revision)
+	if cacheHit {
+		return cachedValue, nil
+	}
+
+	request := smapi.AccessSecretVersionRequest{
+		SecretID: secretID,
+		Revision: fmt.Sprintf("%d", revision),
+	}
+
+	response, err := c.api.AccessSecretVersion(&request, scw.WithContext(ctx))
+	if err != nil {
+		return nil, err
+	}
+
+	return response.Data, nil
+}
+
+func jsonToSecretData(value json.RawMessage) []byte {
+	var stringValue string
+	err := json.Unmarshal(value, &stringValue)
+	if err == nil {
+		return []byte(stringValue)
+	}
+
+	return []byte(strings.TrimSpace(string(value)))
+}
+
+func extractJSONProperty(secretData []byte, property string) ([]byte, error) {
+	result := gjson.Get(string(secretData), property)
+
+	if !result.Exists() {
+		return nil, esv1beta1.NoSecretError{}
+	}
+
+	return jsonToSecretData(json.RawMessage(result.Raw)), nil
+}

+ 386 - 0
pkg/provider/scaleway/client_test.go

@@ -0,0 +1,386 @@
+/*
+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 scaleway
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+var db = buildDB(&fakeSecretAPI{
+	secrets: []*fakeSecret{
+		{
+			name: "secret-1",
+			versions: []*fakeSecretVersion{
+				{revision: 1},
+				{revision: 2},
+				{revision: 3, status: "disabled"},
+			},
+		},
+		{
+			name: "secret-2",
+			tags: []string{"secret-2-tag-1", "secret-2-tag-2"},
+			versions: []*fakeSecretVersion{
+				{revision: 1},
+				{revision: 2},
+			},
+		},
+		{
+			name:     "push-me",
+			versions: []*fakeSecretVersion{},
+		},
+		{
+			name: "not-changed",
+			versions: []*fakeSecretVersion{
+				{revision: 1},
+			},
+		},
+		{
+			name: "disabling-old-versions",
+			versions: []*fakeSecretVersion{
+				{revision: 1},
+			},
+		},
+		{
+			name: "json-data",
+			versions: []*fakeSecretVersion{
+				{
+					revision: 1,
+					data:     []byte(`{"some_string": "abc def", "some_int": -100, "some_bool": false}`),
+				},
+			},
+		},
+		{
+			name: "cant-push",
+			versions: []*fakeSecretVersion{
+				{revision: 1},
+			},
+		},
+		{
+			name: "json-nested",
+			versions: []*fakeSecretVersion{
+				{revision: 1, data: []byte(
+					`{"root":{"intermediate":{"leaf":9}}}`,
+				)},
+			},
+		},
+	},
+})
+
+func newTestClient() esv1beta1.SecretsClient {
+	return &client{
+		api:   db,
+		cache: newCache(),
+	}
+}
+
+func TestGetSecret(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient()
+
+	secret := db.secrets[0]
+
+	testCases := map[string]struct {
+		ref      esv1beta1.ExternalSecretDataRemoteRef
+		response []byte
+		err      error
+	}{
+		"empty version should mean latest_enabled": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:     "id:" + secret.id,
+				Version: "",
+			},
+			response: secret.versions[1].data,
+		},
+		"asking for latest version": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:     "id:" + secret.id,
+				Version: "latest",
+			},
+			response: secret.versions[2].data,
+		},
+		"asking for latest version by name": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:     "name:" + secret.name,
+				Version: "latest",
+			},
+			response: secret.versions[2].data,
+		},
+		"asking for version by revision number": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:     "id:" + secret.id,
+				Version: "1",
+			},
+			response: secret.versions[0].data,
+		},
+		"asking for version by revision number and name": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:     "name:" + secret.name,
+				Version: "1",
+			},
+			response: secret.versions[0].data,
+		},
+		"asking for nested json property": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:      "id:" + db.secret("json-nested").id,
+				Property: "root.intermediate.leaf",
+				Version:  "latest",
+			},
+			response: []byte("9"),
+		},
+		"non existing secret id should yield NoSecretErr": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key: "id:730aa98d-ec0c-4426-8202-b11aeec8ea1e",
+			},
+			err: esv1beta1.NoSecretErr,
+		},
+		"non existing secret name should yield NoSecretErr": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key: "name:not-a-secret",
+			},
+			err: esv1beta1.NoSecretErr,
+		},
+		"non existing revision should yield NoSecretErr": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:     "id:" + secret.id,
+				Version: "9999",
+			},
+			err: esv1beta1.NoSecretErr,
+		},
+		"non existing json property should yield not found": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:      "id:" + db.secret("json-nested").id,
+				Property: "root.intermediate.missing",
+				Version:  "latest",
+			},
+			err: esv1beta1.NoSecretErr,
+		},
+	}
+
+	for tcName, tc := range testCases {
+		t.Run(tcName, func(t *testing.T) {
+			response, err := c.GetSecret(ctx, tc.ref)
+			if tc.err == nil {
+				assert.NoError(t, err)
+				assert.Equal(t, tc.response, response)
+			} else {
+				assert.Nil(t, response)
+				assert.ErrorIs(t, err, tc.err)
+				assert.Equal(t, tc.err, err)
+			}
+		})
+	}
+}
+
+type pushRemoteRef string
+
+func (ref pushRemoteRef) GetRemoteKey() string {
+	return string(ref)
+}
+
+func TestPushSecret(t *testing.T) {
+	t.Run("to new secret", func(t *testing.T) {
+		ctx := context.Background()
+		c := newTestClient()
+		data := []byte("some secret data 6a8ff33b-c69a-4e42-b162-b7b595ee7f5f")
+		secretName := "secret-creation-test"
+
+		pushErr := c.PushSecret(ctx, data, pushRemoteRef("name:"+secretName))
+
+		assert.NoError(t, pushErr)
+		assert.Len(t, db.secret(secretName).versions, 1)
+		assert.Equal(t, data, db.secret(secretName).versions[0].data)
+	})
+
+	t.Run("to secret created by us", func(t *testing.T) {
+		ctx := context.Background()
+		c := newTestClient()
+		data := []byte("some secret data a11d416b-9169-4f4a-8c27-d2959b22e189")
+		secretName := "secret-update-test"
+		assert.NoError(t, c.PushSecret(ctx, []byte("original data"), pushRemoteRef("name:"+secretName)))
+
+		pushErr := c.PushSecret(ctx, data, pushRemoteRef("name:"+secretName))
+
+		assert.NoError(t, pushErr)
+		assert.Len(t, db.secret(secretName).versions, 2)
+		assert.Equal(t, data, db.secret(secretName).versions[1].data)
+	})
+
+	t.Run("to secret partially created by us with no version", func(t *testing.T) {
+		ctx := context.Background()
+		c := newTestClient()
+		data := []byte("some secret data a11d416b-9169-4f4a-8c27-d2959b22e189")
+		secretName := "push-me"
+
+		pushErr := c.PushSecret(ctx, data, pushRemoteRef("name:"+secretName))
+
+		assert.NoError(t, pushErr)
+		assert.Len(t, db.secret(secretName).versions, 1)
+		assert.Equal(t, data, db.secret(secretName).versions[0].data)
+	})
+
+	t.Run("by invalid secret ref is an error", func(t *testing.T) {
+		ctx := context.Background()
+		c := newTestClient()
+
+		pushErr := c.PushSecret(ctx, []byte("some data"), pushRemoteRef("invalid:abcd"))
+
+		assert.Error(t, pushErr)
+	})
+
+	t.Run("by id is an error", func(t *testing.T) {
+		ctx := context.Background()
+		c := newTestClient()
+
+		pushErr := c.PushSecret(ctx, []byte("some data"), pushRemoteRef("id:"+db.secret("cant-push").id))
+
+		assert.Error(t, pushErr)
+	})
+
+	t.Run("without change does not create a version", func(t *testing.T) {
+		ctx := context.Background()
+		c := newTestClient()
+		secret := db.secret("not-changed")
+
+		pushErr := c.PushSecret(ctx, secret.versions[0].data, pushRemoteRef("name:"+secret.name))
+
+		assert.NoError(t, pushErr)
+		assert.Equal(t, 1, len(secret.versions))
+	})
+
+	t.Run("previous version is disabled", func(t *testing.T) {
+		ctx := context.Background()
+		c := newTestClient()
+		secret := db.secret("disabling-old-versions")
+
+		pushErr := c.PushSecret(ctx, []byte("some new data"), pushRemoteRef("name:"+secret.name))
+
+		assert.NoError(t, pushErr)
+		assert.Equal(t, 2, len(secret.versions))
+		assert.Equal(t, "disabled", secret.versions[0].status)
+	})
+}
+
+func TestGetSecretMap(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient()
+
+	values, getErr := c.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{
+		Key:     "id:" + db.secret("json-data").id,
+		Version: "latest",
+	})
+
+	assert.NoError(t, getErr)
+	assert.Equal(t, map[string][]byte{
+		"some_string": []byte("abc def"),
+		"some_int":    []byte("-100"),
+		"some_bool":   []byte("false"),
+	}, values)
+}
+
+func TestGetSecretMapNested(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient()
+
+	values, getErr := c.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{
+		Key:      "id:" + db.secret("json-nested").id,
+		Property: "root.intermediate",
+		Version:  "latest",
+	})
+
+	assert.NoError(t, getErr)
+	assert.Equal(t, map[string][]byte{
+		"leaf": []byte("9"),
+	}, values)
+}
+
+func TestGetAllSecrets(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient()
+
+	testCases := map[string]struct {
+		ref      esv1beta1.ExternalSecretFind
+		response map[string][]byte
+		err      error
+	}{
+		"find secrets by name": {
+			ref: esv1beta1.ExternalSecretFind{
+				Name: &esv1beta1.FindName{RegExp: "secret-.*"},
+			},
+			response: map[string][]byte{
+				db.secret("secret-1").name: db.secret("secret-1").mustGetVersion("latest_enabled").data,
+				db.secret("secret-2").name: db.secret("secret-2").mustGetVersion("latest_enabled").data,
+			},
+		},
+		"find secrets by tags": {
+			ref: esv1beta1.ExternalSecretFind{
+				Tags: map[string]string{"secret-2-tag-1": "ignored-value"},
+			},
+			response: map[string][]byte{
+				db.secrets[1].name: db.secrets[1].mustGetVersion("latest").data,
+			},
+		},
+	}
+
+	for tcName, tc := range testCases {
+		t.Run(tcName, func(t *testing.T) {
+			response, err := c.GetAllSecrets(ctx, tc.ref)
+			if tc.err == nil {
+				assert.NoError(t, err)
+				assert.Equal(t, tc.response, response)
+			} else {
+				assert.Nil(t, response)
+				assert.ErrorIs(t, err, tc.err)
+				assert.Equal(t, tc.err, err)
+			}
+		})
+	}
+}
+
+func TestDeleteSecret(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient()
+
+	secret := db.secrets[0]
+
+	testCases := map[string]struct {
+		ref esv1beta1.PushRemoteRef
+		err error
+	}{
+		"Delete Successfully": {
+			ref: pushRemoteRef("name:" + secret.name),
+			err: nil,
+		},
+		"Secret Not Found": {
+			ref: pushRemoteRef("name:not-a-secret"),
+			err: nil,
+		},
+	}
+
+	for tcName, tc := range testCases {
+		t.Run(tcName, func(t *testing.T) {
+			err := c.DeleteSecret(ctx, tc.ref)
+			if tc.err == nil {
+				assert.NoError(t, err)
+			} else {
+				assert.ErrorIs(t, err, tc.err)
+				assert.Equal(t, tc.err, err)
+			}
+		})
+	}
+}

+ 441 - 0
pkg/provider/scaleway/fake_secret_api_test.go

@@ -0,0 +1,441 @@
+/*
+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 scaleway
+
+import (
+	"fmt"
+	"sort"
+	"strconv"
+
+	"github.com/google/uuid"
+	smapi "github.com/scaleway/scaleway-sdk-go/api/secret/v1alpha1"
+	"github.com/scaleway/scaleway-sdk-go/scw"
+)
+
+type fakeSecretVersion struct {
+	revision     int
+	data         []byte
+	dontFillData bool
+	status       string
+}
+
+type fakeSecret struct {
+	id       string
+	name     string
+	versions []*fakeSecretVersion
+	tags     []string
+	status   string
+}
+
+type fakeSecretAPI struct {
+	secrets        []*fakeSecret
+	_secretsByID   map[string]*fakeSecret
+	_secretsByName map[string]*fakeSecret
+}
+
+func buildDB(f *fakeSecretAPI) *fakeSecretAPI {
+	f._secretsByID = map[string]*fakeSecret{}
+	f._secretsByName = map[string]*fakeSecret{}
+
+	for _, secret := range f.secrets {
+		if secret.id == "" {
+			secret.id = uuid.NewString()
+		}
+
+		sort.Slice(secret.versions, func(i, j int) bool {
+			return secret.versions[i].revision < secret.versions[j].revision
+		})
+
+		for index, version := range secret.versions {
+			if version.revision != index+1 {
+				panic("bad revision number in fixtures")
+			}
+
+			if version.status == "" {
+				version.status = "enabled"
+			}
+		}
+
+		for _, version := range secret.versions {
+			if len(version.data) == 0 && !version.dontFillData {
+				version.data = []byte(fmt.Sprintf("some data for secret %s version %d: %s", secret.id, version.revision, uuid.NewString()))
+			}
+		}
+
+		if secret.status == "" {
+			secret.status = "ready"
+		}
+
+		f._secretsByID[secret.id] = secret
+		f._secretsByName[secret.name] = secret
+	}
+
+	return f
+}
+
+func (s *fakeSecret) getVersion(revision string) (*fakeSecretVersion, bool) {
+	if len(s.versions) == 0 {
+		return nil, false
+	}
+
+	if revision == "latest" {
+		return s.versions[len(s.versions)-1], true
+	}
+
+	if revision == "latest_enabled" {
+		for i := len(s.versions) - 1; i >= 0; i-- {
+			if s.versions[i].status == "enabled" {
+				return s.versions[i], true
+			}
+		}
+		return nil, false
+	}
+
+	revisionNumber, err := strconv.Atoi(revision)
+	if err != nil {
+		return nil, false
+	}
+
+	i, found := sort.Find(len(s.versions), func(i int) int {
+		if revisionNumber < s.versions[i].revision {
+			return -1
+		} else if revisionNumber > s.versions[i].revision {
+			return 1
+		} else {
+			return 0
+		}
+	})
+	if found {
+		return s.versions[i], true
+	}
+	return nil, false
+}
+
+func (s *fakeSecret) mustGetVersion(revision string) *fakeSecretVersion {
+	version, ok := s.getVersion(revision)
+	if !ok {
+		panic("no such version")
+	}
+
+	return version
+}
+
+func (f *fakeSecretAPI) secret(name string) *fakeSecret {
+	return f._secretsByName[name]
+}
+
+func (f *fakeSecretAPI) getSecretByID(secretID string) (*fakeSecret, error) {
+	secret, foundSecret := f._secretsByID[secretID]
+
+	if !foundSecret {
+		return nil, &scw.ResourceNotFoundError{
+			Resource:   "secret",
+			ResourceID: secretID,
+		}
+	}
+
+	return secret, nil
+}
+
+func (f *fakeSecretAPI) getSecretByName(secretName string) (*fakeSecret, error) {
+	secret, foundSecret := f._secretsByName[secretName]
+
+	if !foundSecret {
+		return nil, &scw.ResourceNotFoundError{
+			Resource:   "secret",
+			ResourceID: secretName,
+		}
+	}
+
+	return secret, nil
+}
+
+func (f *fakeSecretAPI) GetSecret(request *smapi.GetSecretRequest, _ ...scw.RequestOption) (*smapi.Secret, error) {
+	if request.Region != "" {
+		panic("explicit region in request is not supported")
+	}
+
+	secret, err := f.getSecretByID(request.SecretID)
+	if err != nil {
+		return nil, err
+	}
+
+	return &smapi.Secret{
+		ID:           secret.id,
+		Name:         secret.name,
+		Status:       smapi.SecretStatus(secret.status),
+		Tags:         secret.tags,
+		VersionCount: uint32(len(secret.versions)),
+	}, nil
+}
+
+func (f *fakeSecretAPI) GetSecretByName(request *smapi.GetSecretByNameRequest, _ ...scw.RequestOption) (*smapi.Secret, error) {
+	if request.Region != "" {
+		panic("explicit region in request is not supported")
+	}
+
+	secret, err := f.getSecretByName(request.SecretName)
+	if err != nil {
+		return nil, err
+	}
+
+	return &smapi.Secret{
+		ID:           secret.id,
+		Name:         secret.name,
+		Status:       smapi.SecretStatus(secret.status),
+		Tags:         secret.tags,
+		VersionCount: uint32(len(secret.versions)),
+	}, nil
+}
+
+func (f *fakeSecretAPI) GetSecretVersion(request *smapi.GetSecretVersionRequest, _ ...scw.RequestOption) (*smapi.SecretVersion, error) {
+	if request.Region != "" {
+		panic("explicit region in request is not supported")
+	}
+
+	secret, err := f.getSecretByID(request.SecretID)
+	if err != nil {
+		return nil, err
+	}
+
+	version, ok := secret.getVersion(request.Revision)
+	if !ok {
+		return nil, &scw.ResourceNotFoundError{
+			Resource:   "secret_version",
+			ResourceID: request.Revision,
+		}
+	}
+
+	return &smapi.SecretVersion{
+		SecretID: secret.id,
+		Revision: uint32(version.revision),
+		Status:   smapi.SecretVersionStatus(secret.status),
+	}, nil
+}
+
+func (f *fakeSecretAPI) GetSecretVersionByName(request *smapi.GetSecretVersionByNameRequest, _ ...scw.RequestOption) (*smapi.SecretVersion, error) {
+	if request.Region != "" {
+		panic("explicit region in request is not supported")
+	}
+
+	secret, err := f.getSecretByName(request.SecretName)
+	if err != nil {
+		return nil, err
+	}
+
+	version, ok := secret.getVersion(request.Revision)
+	if !ok {
+		return nil, &scw.ResourceNotFoundError{
+			Resource:   "secret_version",
+			ResourceID: request.Revision,
+		}
+	}
+
+	return &smapi.SecretVersion{
+		SecretID: secret.id,
+		Revision: uint32(version.revision),
+		Status:   smapi.SecretVersionStatus(secret.status),
+	}, nil
+}
+
+func (f *fakeSecretAPI) AccessSecretVersion(request *smapi.AccessSecretVersionRequest, _ ...scw.RequestOption) (*smapi.AccessSecretVersionResponse, error) {
+	if request.Region != "" {
+		panic("explicit region in request is not supported")
+	}
+
+	secret, err := f.getSecretByID(request.SecretID)
+	if err != nil {
+		return nil, err
+	}
+
+	version, ok := secret.getVersion(request.Revision)
+	if !ok {
+		return nil, &scw.ResourceNotFoundError{
+			Resource:   "secret_version",
+			ResourceID: request.Revision,
+		}
+	}
+
+	return &smapi.AccessSecretVersionResponse{
+		SecretID: secret.id,
+		Revision: uint32(version.revision),
+		Data:     version.data,
+	}, nil
+}
+
+func (f *fakeSecretAPI) DisableSecretVersion(request *smapi.DisableSecretVersionRequest, _ ...scw.RequestOption) (*smapi.SecretVersion, error) {
+	if request.Region != "" {
+		panic("explicit region in request is not supported")
+	}
+
+	secret, err := f.getSecretByID(request.SecretID)
+	if err != nil {
+		return nil, err
+	}
+
+	version, ok := secret.getVersion(request.Revision)
+	if !ok {
+		return nil, &scw.ResourceNotFoundError{
+			Resource:   "secret_version",
+			ResourceID: request.Revision,
+		}
+	}
+
+	version.status = "disabled"
+
+	return &smapi.SecretVersion{
+		SecretID: secret.id,
+		Revision: uint32(version.revision),
+		Status:   smapi.SecretVersionStatus(version.status),
+	}, nil
+}
+
+func matchListSecretFilter(secret *fakeSecret, filter *smapi.ListSecretsRequest) bool {
+	for _, requiredTag := range filter.Tags {
+		found := false
+
+		for _, secretTag := range secret.tags {
+			if requiredTag == secretTag {
+				found = true
+				break
+			}
+		}
+
+		if !found {
+			return false
+		}
+	}
+
+	return true
+}
+
+func (f *fakeSecretAPI) ListSecrets(request *smapi.ListSecretsRequest, _ ...scw.RequestOption) (*smapi.ListSecretsResponse, error) {
+	var matches []*fakeSecret
+
+	// filtering
+
+	for _, secret := range f.secrets {
+		if matchListSecretFilter(secret, request) {
+			matches = append(matches, secret)
+		}
+	}
+
+	// ordering
+
+	if request.OrderBy != "" {
+		panic("explicit order by is not implemented")
+	}
+
+	sort.Slice(matches, func(i, j int) bool {
+		return matches[i].id >= matches[j].id
+	})
+
+	// pagination
+
+	response := smapi.ListSecretsResponse{
+		TotalCount: uint32(len(matches)),
+	}
+
+	if request.Page == nil || request.PageSize == nil {
+		panic("list secrets without explicit pagination not implemented")
+	}
+	page := int(*request.Page)
+	pageSize := int(*request.PageSize)
+
+	startOffset := (page - 1) * pageSize
+	if startOffset > len(matches) {
+		return nil, fmt.Errorf("invalid page offset (page = %d, page size = %d, total = %d)", page, pageSize, len(matches))
+	}
+
+	endOffset := page * pageSize
+	if endOffset > len(matches) {
+		endOffset = len(matches)
+	}
+
+	for _, secret := range matches[startOffset:endOffset] {
+		response.Secrets = append(response.Secrets, &smapi.Secret{
+			ID:           secret.id,
+			Name:         secret.name,
+			Status:       smapi.SecretStatus(secret.status),
+			Tags:         secret.tags,
+			VersionCount: uint32(len(secret.versions)),
+		})
+	}
+
+	return &response, nil
+}
+
+func (f *fakeSecretAPI) CreateSecret(request *smapi.CreateSecretRequest, _ ...scw.RequestOption) (*smapi.Secret, error) {
+	if request.Region != "" {
+		panic("explicit region in request is not supported")
+	}
+
+	secret := &fakeSecret{
+		id:     uuid.NewString(),
+		name:   request.Name,
+		status: "ready",
+	}
+
+	f.secrets = append(f.secrets, secret)
+	f._secretsByID[secret.id] = secret
+	f._secretsByName[secret.name] = secret
+
+	return &smapi.Secret{
+		ID:           secret.id,
+		ProjectID:    request.ProjectID,
+		Name:         secret.name,
+		Status:       smapi.SecretStatus(secret.status),
+		VersionCount: 0,
+	}, nil
+}
+
+func (f *fakeSecretAPI) CreateSecretVersion(request *smapi.CreateSecretVersionRequest, _ ...scw.RequestOption) (*smapi.SecretVersion, error) {
+	if request.Region != "" {
+		panic("explicit region in request is not supported")
+	}
+
+	secret, ok := f._secretsByID[request.SecretID]
+	if !ok {
+		return nil, &scw.ResourceNotFoundError{
+			Resource:   "secret",
+			ResourceID: request.SecretID,
+		}
+	}
+
+	newVersion := &fakeSecretVersion{
+		revision: len(secret.versions) + 1,
+		data:     request.Data,
+	}
+
+	secret.versions = append(secret.versions, newVersion)
+
+	return &smapi.SecretVersion{
+		SecretID: request.SecretID,
+		Revision: uint32(newVersion.revision),
+		Status:   smapi.SecretVersionStatus(newVersion.status),
+	}, nil
+}
+
+func (f *fakeSecretAPI) DeleteSecret(request *smapi.DeleteSecretRequest, _ ...scw.RequestOption) error {
+	secret, ok := f._secretsByID[request.SecretID]
+	if !ok {
+		return &scw.ResourceNotFoundError{
+			Resource:   "secret",
+			ResourceID: request.SecretID,
+		}
+	}
+	delete(f._secretsByID, secret.id)
+
+	return nil
+}

+ 195 - 0
pkg/provider/scaleway/provider.go

@@ -0,0 +1,195 @@
+/*
+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 scaleway
+
+import (
+	"context"
+	"fmt"
+
+	smapi "github.com/scaleway/scaleway-sdk-go/api/secret/v1alpha1"
+	"github.com/scaleway/scaleway-sdk-go/scw"
+	"github.com/scaleway/scaleway-sdk-go/validation"
+	corev1 "k8s.io/api/core/v1"
+	ctrl "sigs.k8s.io/controller-runtime"
+	kubeClient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+var (
+	defaultAPIURL = "https://api.scaleway.com"
+	log           = ctrl.Log.WithName("provider").WithName("scaleway")
+)
+
+type Provider struct{}
+
+// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
+func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
+	return esv1beta1.SecretStoreReadWrite
+}
+
+func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kubeClient.Client, namespace string) (esv1beta1.SecretsClient, error) {
+	cfg, err := getConfig(store)
+	if err != nil {
+		return nil, err
+	}
+
+	if store.GetKind() == esv1beta1.ClusterSecretStoreKind && doesConfigDependOnNamespace(cfg) {
+		// we are not attached to a specific namespace, but some config values are dependent on it
+		return nil, fmt.Errorf("when using a ClusterSecretStore, namespaces must be explicitly set")
+	}
+
+	accessKey, err := loadConfigSecret(ctx, cfg.AccessKey, kube, namespace)
+	if err != nil {
+		return nil, err
+	}
+
+	secretKey, err := loadConfigSecret(ctx, cfg.SecretKey, kube, namespace)
+	if err != nil {
+		return nil, err
+	}
+
+	scwClient, err := scw.NewClient(
+		scw.WithAPIURL(cfg.APIURL),
+		scw.WithDefaultRegion(scw.Region(cfg.Region)),
+		scw.WithDefaultProjectID(cfg.ProjectID),
+		scw.WithAuth(accessKey, secretKey),
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return &client{
+		api:       smapi.NewAPI(scwClient),
+		projectID: cfg.ProjectID,
+		cache:     newCache(),
+	}, nil
+}
+
+func loadConfigSecret(ctx context.Context, ref *esv1beta1.ScalewayProviderSecretRef, kube kubeClient.Client, defaultNamespace string) (string, error) {
+	if ref.SecretRef == nil {
+		return ref.Value, nil
+	}
+
+	namespace := defaultNamespace
+	if ref.SecretRef.Namespace != nil {
+		namespace = *ref.SecretRef.Namespace
+	}
+
+	if ref.SecretRef.Name == "" {
+		return "", fmt.Errorf("must specify a value or a reference to a secret")
+	}
+
+	if ref.SecretRef.Key == "" {
+		return "", fmt.Errorf("must specify a secret key")
+	}
+
+	objKey := kubeClient.ObjectKey{
+		Namespace: namespace,
+		Name:      ref.SecretRef.Name,
+	}
+
+	secret := corev1.Secret{}
+
+	err := kube.Get(ctx, objKey, &secret)
+	if err != nil {
+		return "", err
+	}
+
+	value, ok := secret.Data[ref.SecretRef.Key]
+	if !ok {
+		return "", fmt.Errorf("no such key in secret: %v", ref.SecretRef.Key)
+	}
+
+	return string(value), nil
+}
+
+func validateSecretRef(store esv1beta1.GenericStore, ref *esv1beta1.ScalewayProviderSecretRef) error {
+	if ref.SecretRef != nil {
+		if ref.Value != "" {
+			return fmt.Errorf("cannot specify both secret reference and value")
+		}
+		err := utils.ValidateReferentSecretSelector(store, *ref.SecretRef)
+		if err != nil {
+			return err
+		}
+	} else if ref.Value == "" {
+		return fmt.Errorf("must specify either secret reference or direct value")
+	}
+
+	return nil
+}
+
+func doesConfigDependOnNamespace(cfg *esv1beta1.ScalewayProvider) bool {
+	if cfg.AccessKey.SecretRef != nil && cfg.AccessKey.SecretRef.Namespace != nil {
+		return true
+	}
+
+	if cfg.SecretKey.SecretRef != nil && cfg.SecretKey.SecretRef.Namespace != nil {
+		return true
+	}
+
+	return false
+}
+
+func getConfig(store esv1beta1.GenericStore) (*esv1beta1.ScalewayProvider, error) {
+	if store == nil {
+		return nil, fmt.Errorf("missing store specification")
+	}
+	storeSpec := store.GetSpec()
+
+	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Scaleway == nil {
+		return nil, fmt.Errorf("invalid specification for scaleway provider")
+	}
+	cfg := storeSpec.Provider.Scaleway
+
+	if cfg.APIURL == "" {
+		cfg.APIURL = defaultAPIURL
+	} else if !validation.IsURL(cfg.APIURL) {
+		return nil, fmt.Errorf("invalid api url: %q", cfg.APIURL)
+	}
+
+	if !validation.IsRegion(cfg.Region) {
+		return nil, fmt.Errorf("invalid region: %q", cfg.Region)
+	}
+
+	if !validation.IsProjectID(cfg.ProjectID) {
+		return nil, fmt.Errorf("invalid project id: %q", cfg.ProjectID)
+	}
+
+	err := validateSecretRef(store, cfg.AccessKey)
+	if err != nil {
+		return nil, err
+	}
+
+	err = validateSecretRef(store, cfg.SecretKey)
+	if err != nil {
+		return nil, err
+	}
+
+	return cfg, nil
+}
+
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
+	_, err := getConfig(store)
+	return err
+}
+
+func init() {
+	esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+		Scaleway: &esv1beta1.ScalewayProvider{},
+	})
+}

+ 32 - 0
pkg/provider/scaleway/secret_api.go

@@ -0,0 +1,32 @@
+/*
+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 scaleway
+
+import (
+	smapi "github.com/scaleway/scaleway-sdk-go/api/secret/v1alpha1"
+	"github.com/scaleway/scaleway-sdk-go/scw"
+)
+
+type secretAPI interface {
+	GetSecret(req *smapi.GetSecretRequest, opts ...scw.RequestOption) (*smapi.Secret, error)
+	GetSecretByName(req *smapi.GetSecretByNameRequest, opts ...scw.RequestOption) (*smapi.Secret, error)
+	GetSecretVersion(req *smapi.GetSecretVersionRequest, opts ...scw.RequestOption) (*smapi.SecretVersion, error)
+	GetSecretVersionByName(req *smapi.GetSecretVersionByNameRequest, opts ...scw.RequestOption) (*smapi.SecretVersion, error)
+	AccessSecretVersion(request *smapi.AccessSecretVersionRequest, option ...scw.RequestOption) (*smapi.AccessSecretVersionResponse, error)
+	DisableSecretVersion(request *smapi.DisableSecretVersionRequest, option ...scw.RequestOption) (*smapi.SecretVersion, error)
+	ListSecrets(request *smapi.ListSecretsRequest, option ...scw.RequestOption) (*smapi.ListSecretsResponse, error)
+	CreateSecret(request *smapi.CreateSecretRequest, option ...scw.RequestOption) (*smapi.Secret, error)
+	CreateSecretVersion(request *smapi.CreateSecretVersionRequest, option ...scw.RequestOption) (*smapi.SecretVersion, error)
+	DeleteSecret(request *smapi.DeleteSecretRequest, option ...scw.RequestOption) error
+}