Browse Source

feat: ovh provider implementation (#6101)

Co-authored-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Co-authored-by: Gergely Bräutigam <gergely.brautigam@sap.com>
Aeddis Desauw 2 weeks ago
parent
commit
a15be5dff5
38 changed files with 4807 additions and 0 deletions
  1. 1 0
      .github/CODEOWNERS.md
  2. 1 0
      CODEOWNERS.md
  3. 66 0
      apis/externalsecrets/v1/secretstore_ovh_types.go
  4. 4 0
      apis/externalsecrets/v1/secretstore_types.go
  5. 99 0
      apis/externalsecrets/v1/zz_generated.deepcopy.go
  6. 174 0
      config/crds/bases/external-secrets.io_clustersecretstores.yaml
  7. 174 0
      config/crds/bases/external-secrets.io_secretstores.yaml
  8. 324 0
      deploy/crds/bundle.yaml
  9. 236 0
      docs/api/spec.md
  10. 508 0
      docs/provider/ovhcloud.md
  11. 5 0
      go.mod
  12. 10 0
      go.sum
  13. 1 0
      hack/api-docs/mkdocs.yml
  14. 30 0
      pkg/register/ovh.go
  15. 25 0
      providers/v1/ovh/client_close.go
  16. 42 0
      providers/v1/ovh/client_delete_secret.go
  17. 208 0
      providers/v1/ovh/client_get_all_secrets.go
  18. 169 0
      providers/v1/ovh/client_get_all_secrets_test.go
  19. 41 0
      providers/v1/ovh/client_get_secret.go
  20. 70 0
      providers/v1/ovh/client_get_secret_map.go
  21. 143 0
      providers/v1/ovh/client_get_secret_map_test.go
  22. 136 0
      providers/v1/ovh/client_get_secret_test.go
  23. 210 0
      providers/v1/ovh/client_push_secret.go
  24. 159 0
      providers/v1/ovh/client_push_secret_test.go
  25. 41 0
      providers/v1/ovh/client_secret_exists.go
  26. 86 0
      providers/v1/ovh/client_secret_exists_test.go
  27. 124 0
      providers/v1/ovh/client_utils.go
  28. 223 0
      providers/v1/ovh/fake/fake_okms_client.go
  29. 85 0
      providers/v1/ovh/fake/fake_resolver.go
  30. 68 0
      providers/v1/ovh/fake/fake_resolver_test.go
  31. 113 0
      providers/v1/ovh/go.mod
  32. 270 0
      providers/v1/ovh/go.sum
  33. 355 0
      providers/v1/ovh/provider.go
  34. 455 0
      providers/v1/ovh/provider_test.go
  35. 35 0
      providers/v1/ovh/validate.go
  36. 64 0
      providers/v1/ovh/validate_test.go
  37. 26 0
      tests/__snapshot__/clustersecretstore-v1.yaml
  38. 26 0
      tests/__snapshot__/secretstore-v1.yaml

+ 1 - 0
.github/CODEOWNERS.md

@@ -47,6 +47,7 @@ providers/v1/onboardbase/             @external-secrets/provider-onboardbase-rev
 providers/v1/onepassword/             @external-secrets/provider-onepassword-reviewers
 providers/v1/onepasswordsdk/          @external-secrets/provider-onepasswordsdk-reviewers
 providers/v1/oracle/                  @external-secrets/provider-oracle-reviewers
+providers/v1/ovh/                      @external-secrets/provider-ovh-reviewers
 providers/v1/passbolt/                @external-secrets/provider-passbolt-reviewers
 providers/v1/passworddepot/           @external-secrets/provider-passworddepot-reviewers
 providers/v1/previder/                @external-secrets/provider-previder-reviewers

+ 1 - 0
CODEOWNERS.md

@@ -47,6 +47,7 @@ providers/v1/onboardbase/             @external-secrets/provider-onboardbase-rev
 providers/v1/onepassword/             @external-secrets/provider-onepassword-reviewers
 providers/v1/onepasswordsdk/          @external-secrets/provider-onepasswordsdk-reviewers
 providers/v1/oracle/                  @external-secrets/provider-oracle-reviewers
+pkg/provider/v1/ovh/                  @external-secrets/provider-ovh-reviewers
 providers/v1/passbolt/                @external-secrets/provider-passbolt-reviewers
 providers/v1/passworddepot/           @external-secrets/provider-passworddepot-reviewers
 providers/v1/previder/                @external-secrets/provider-previder-reviewers

+ 66 - 0
apis/externalsecrets/v1/secretstore_ovh_types.go

@@ -0,0 +1,66 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 v1
+
+import esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+
+// OvhProvider holds the configuration to synchronize secrets with OVHcloud's Secret Manager.
+type OvhProvider struct {
+	// specifies the OKMS server endpoint.
+	// +required
+	Server string `json:"server"`
+	// specifies the OKMS ID.
+	// +required
+	OkmsID string `json:"okmsid"`
+	// Enables or disables check-and-set (CAS) (default: false).
+	// +optional
+	CasRequired *bool `json:"casRequired,omitempty"`
+	// Setup a timeout in seconds when requests to the KMS are made (default: 30).
+	// +optional
+	// +kubebuilder:validation:Minimum=1
+	// +kubebuilder:default=30
+	OkmsTimeout *uint32 `json:"okmsTimeout,omitempty"`
+	// Authentication method (mtls or token).
+	// +required
+	Auth OvhAuth `json:"auth"`
+}
+
+// OvhAuth tells the controller how to authenticate to OVHcloud's Secret Manager, either using mTLS or a token.
+type OvhAuth struct {
+	// +optional
+	ClientMTLS *OvhClientMTLS `json:"mtls,omitempty"`
+	// +optional
+	ClientToken *OvhClientToken `json:"token,omitempty"`
+}
+
+// OvhClientMTLS defines the configuration required to authenticate to OVHcloud's Secret Manager using mTLS.
+type OvhClientMTLS struct {
+	// +required
+	ClientCertificate esmeta.SecretKeySelector `json:"certSecretRef"`
+	// +required
+	ClientKey esmeta.SecretKeySelector `json:"keySecretRef"`
+	// +optional
+	CABundle []byte `json:"caBundle,omitempty"`
+	// +optional
+	CAProvider *CAProvider `json:"caProvider,omitempty"`
+}
+
+// OvhClientToken defines the configuration required to authenticate to OVHcloud's Secret Manager using a token.
+type OvhClientToken struct {
+	// +required
+	ClientTokenSecret esmeta.SecretKeySelector `json:"tokenSecretRef"`
+}

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

@@ -87,6 +87,10 @@ type SecretStoreProvider struct {
 	// +optional
 	Vault *VaultProvider `json:"vault,omitempty"`
 
+	// OVHcloud configures this store to sync secrets using the OVHcloud provider.
+	// +optional
+	OVHcloud *OvhProvider `json:"ovh,omitempty"`
+
 	// GCPSM configures this store to sync secrets using Google Cloud Platform Secret Manager provider
 	// +optional
 	GCPSM *GCPSMProvider `json:"gcpsm,omitempty"`

+ 99 - 0
apis/externalsecrets/v1/zz_generated.deepcopy.go

@@ -2996,6 +2996,100 @@ func (in *OracleSecretRef) DeepCopy() *OracleSecretRef {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OvhAuth) DeepCopyInto(out *OvhAuth) {
+	*out = *in
+	if in.ClientMTLS != nil {
+		in, out := &in.ClientMTLS, &out.ClientMTLS
+		*out = new(OvhClientMTLS)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.ClientToken != nil {
+		in, out := &in.ClientToken, &out.ClientToken
+		*out = new(OvhClientToken)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OvhAuth.
+func (in *OvhAuth) DeepCopy() *OvhAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(OvhAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OvhClientMTLS) DeepCopyInto(out *OvhClientMTLS) {
+	*out = *in
+	in.ClientCertificate.DeepCopyInto(&out.ClientCertificate)
+	in.ClientKey.DeepCopyInto(&out.ClientKey)
+	if in.CABundle != nil {
+		in, out := &in.CABundle, &out.CABundle
+		*out = make([]byte, len(*in))
+		copy(*out, *in)
+	}
+	if in.CAProvider != nil {
+		in, out := &in.CAProvider, &out.CAProvider
+		*out = new(CAProvider)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OvhClientMTLS.
+func (in *OvhClientMTLS) DeepCopy() *OvhClientMTLS {
+	if in == nil {
+		return nil
+	}
+	out := new(OvhClientMTLS)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OvhClientToken) DeepCopyInto(out *OvhClientToken) {
+	*out = *in
+	in.ClientTokenSecret.DeepCopyInto(&out.ClientTokenSecret)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OvhClientToken.
+func (in *OvhClientToken) DeepCopy() *OvhClientToken {
+	if in == nil {
+		return nil
+	}
+	out := new(OvhClientToken)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OvhProvider) DeepCopyInto(out *OvhProvider) {
+	*out = *in
+	if in.CasRequired != nil {
+		in, out := &in.CasRequired, &out.CasRequired
+		*out = new(bool)
+		**out = **in
+	}
+	if in.OkmsTimeout != nil {
+		in, out := &in.OkmsTimeout, &out.OkmsTimeout
+		*out = new(uint32)
+		**out = **in
+	}
+	in.Auth.DeepCopyInto(&out.Auth)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OvhProvider.
+func (in *OvhProvider) DeepCopy() *OvhProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(OvhProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PassboltAuth) DeepCopyInto(out *PassboltAuth) {
 	*out = *in
 	if in.PasswordSecretRef != nil {
@@ -3382,6 +3476,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(VaultProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.OVHcloud != nil {
+		in, out := &in.OVHcloud, &out.OVHcloud
+		*out = new(OvhProvider)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.GCPSM != nil {
 		in, out := &in.GCPSM, &out.GCPSM
 		*out = new(GCPSMProvider)

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

@@ -4126,6 +4126,180 @@ spec:
                     - region
                     - vault
                     type: object
+                  ovh:
+                    description: OVHcloud configures this store to sync secrets using
+                      the OVHcloud provider.
+                    properties:
+                      auth:
+                        description: Authentication method (mtls or token).
+                        properties:
+                          mtls:
+                            description: OvhClientMTLS defines the configuration required
+                              to authenticate to OVHcloud's Secret Manager using mTLS.
+                            properties:
+                              caBundle:
+                                format: byte
+                                type: string
+                              caProvider:
+                                description: |-
+                                  CAProvider provides a custom certificate authority for accessing the provider's store.
+                                  The CAProvider points to a Secret or ConfigMap resource that contains a PEM-encoded certificate.
+                                properties:
+                                  key:
+                                    description: The key where the CA certificate
+                                      can be found in the Secret or ConfigMap.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the object located at
+                                      the provider type.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace the Provider type is in.
+                                      Can only be defined when used in a ClusterSecretStore.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                  type:
+                                    description: The type of provider to use such
+                                      as "Secret", or "ConfigMap".
+                                    enum:
+                                    - Secret
+                                    - ConfigMap
+                                    type: string
+                                required:
+                                - name
+                                - type
+                                type: object
+                              certSecretRef:
+                                description: |-
+                                  SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                              keySecretRef:
+                                description: |-
+                                  SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                            required:
+                            - certSecretRef
+                            - keySecretRef
+                            type: object
+                          token:
+                            description: OvhClientToken defines the configuration
+                              required to authenticate to OVHcloud's Secret Manager
+                              using a token.
+                            properties:
+                              tokenSecretRef:
+                                description: |-
+                                  SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                            required:
+                            - tokenSecretRef
+                            type: object
+                        type: object
+                      casRequired:
+                        description: 'Enables or disables check-and-set (CAS) (default:
+                          false).'
+                        type: boolean
+                      okmsTimeout:
+                        default: 30
+                        description: 'Setup a timeout in seconds when requests to
+                          the KMS are made (default: 30).'
+                        format: int32
+                        minimum: 1
+                        type: integer
+                      okmsid:
+                        description: specifies the OKMS ID.
+                        type: string
+                      server:
+                        description: specifies the OKMS server endpoint.
+                        type: string
+                    required:
+                    - auth
+                    - okmsid
+                    - server
+                    type: object
                   passbolt:
                     description: |-
                       PassboltProvider provides access to Passbolt secrets manager.

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

@@ -4126,6 +4126,180 @@ spec:
                     - region
                     - vault
                     type: object
+                  ovh:
+                    description: OVHcloud configures this store to sync secrets using
+                      the OVHcloud provider.
+                    properties:
+                      auth:
+                        description: Authentication method (mtls or token).
+                        properties:
+                          mtls:
+                            description: OvhClientMTLS defines the configuration required
+                              to authenticate to OVHcloud's Secret Manager using mTLS.
+                            properties:
+                              caBundle:
+                                format: byte
+                                type: string
+                              caProvider:
+                                description: |-
+                                  CAProvider provides a custom certificate authority for accessing the provider's store.
+                                  The CAProvider points to a Secret or ConfigMap resource that contains a PEM-encoded certificate.
+                                properties:
+                                  key:
+                                    description: The key where the CA certificate
+                                      can be found in the Secret or ConfigMap.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the object located at
+                                      the provider type.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace the Provider type is in.
+                                      Can only be defined when used in a ClusterSecretStore.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                  type:
+                                    description: The type of provider to use such
+                                      as "Secret", or "ConfigMap".
+                                    enum:
+                                    - Secret
+                                    - ConfigMap
+                                    type: string
+                                required:
+                                - name
+                                - type
+                                type: object
+                              certSecretRef:
+                                description: |-
+                                  SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                              keySecretRef:
+                                description: |-
+                                  SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                            required:
+                            - certSecretRef
+                            - keySecretRef
+                            type: object
+                          token:
+                            description: OvhClientToken defines the configuration
+                              required to authenticate to OVHcloud's Secret Manager
+                              using a token.
+                            properties:
+                              tokenSecretRef:
+                                description: |-
+                                  SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                            required:
+                            - tokenSecretRef
+                            type: object
+                        type: object
+                      casRequired:
+                        description: 'Enables or disables check-and-set (CAS) (default:
+                          false).'
+                        type: boolean
+                      okmsTimeout:
+                        default: 30
+                        description: 'Setup a timeout in seconds when requests to
+                          the KMS are made (default: 30).'
+                        format: int32
+                        minimum: 1
+                        type: integer
+                      okmsid:
+                        description: specifies the OKMS ID.
+                        type: string
+                      server:
+                        description: specifies the OKMS server endpoint.
+                        type: string
+                    required:
+                    - auth
+                    - okmsid
+                    - server
+                    type: object
                   passbolt:
                     description: |-
                       PassboltProvider provides access to Passbolt secrets manager.

+ 324 - 0
deploy/crds/bundle.yaml

@@ -5931,6 +5931,168 @@ spec:
                         - region
                         - vault
                       type: object
+                    ovh:
+                      description: OVHcloud configures this store to sync secrets using the OVHcloud provider.
+                      properties:
+                        auth:
+                          description: Authentication method (mtls or token).
+                          properties:
+                            mtls:
+                              description: OvhClientMTLS defines the configuration required to authenticate to OVHcloud's Secret Manager using mTLS.
+                              properties:
+                                caBundle:
+                                  format: byte
+                                  type: string
+                                caProvider:
+                                  description: |-
+                                    CAProvider provides a custom certificate authority for accessing the provider's store.
+                                    The CAProvider points to a Secret or ConfigMap resource that contains a PEM-encoded certificate.
+                                  properties:
+                                    key:
+                                      description: The key where the CA certificate can be found in the Secret or ConfigMap.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the object located at the provider type.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace the Provider type is in.
+                                        Can only be defined when used in a ClusterSecretStore.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                    type:
+                                      description: The type of provider to use such as "Secret", or "ConfigMap".
+                                      enum:
+                                        - Secret
+                                        - ConfigMap
+                                      type: string
+                                  required:
+                                    - name
+                                    - type
+                                  type: object
+                                certSecretRef:
+                                  description: |-
+                                    SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                                keySecretRef:
+                                  description: |-
+                                    SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                              required:
+                                - certSecretRef
+                                - keySecretRef
+                              type: object
+                            token:
+                              description: OvhClientToken defines the configuration required to authenticate to OVHcloud's Secret Manager using a token.
+                              properties:
+                                tokenSecretRef:
+                                  description: |-
+                                    SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                              required:
+                                - tokenSecretRef
+                              type: object
+                          type: object
+                        casRequired:
+                          description: 'Enables or disables check-and-set (CAS) (default: false).'
+                          type: boolean
+                        okmsTimeout:
+                          default: 30
+                          description: 'Setup a timeout in seconds when requests to the KMS are made (default: 30).'
+                          format: int32
+                          minimum: 1
+                          type: integer
+                        okmsid:
+                          description: specifies the OKMS ID.
+                          type: string
+                        server:
+                          description: specifies the OKMS server endpoint.
+                          type: string
+                      required:
+                        - auth
+                        - okmsid
+                        - server
+                      type: object
                     passbolt:
                       description: |-
                         PassboltProvider provides access to Passbolt secrets manager.
@@ -17699,6 +17861,168 @@ spec:
                         - region
                         - vault
                       type: object
+                    ovh:
+                      description: OVHcloud configures this store to sync secrets using the OVHcloud provider.
+                      properties:
+                        auth:
+                          description: Authentication method (mtls or token).
+                          properties:
+                            mtls:
+                              description: OvhClientMTLS defines the configuration required to authenticate to OVHcloud's Secret Manager using mTLS.
+                              properties:
+                                caBundle:
+                                  format: byte
+                                  type: string
+                                caProvider:
+                                  description: |-
+                                    CAProvider provides a custom certificate authority for accessing the provider's store.
+                                    The CAProvider points to a Secret or ConfigMap resource that contains a PEM-encoded certificate.
+                                  properties:
+                                    key:
+                                      description: The key where the CA certificate can be found in the Secret or ConfigMap.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the object located at the provider type.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace the Provider type is in.
+                                        Can only be defined when used in a ClusterSecretStore.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                    type:
+                                      description: The type of provider to use such as "Secret", or "ConfigMap".
+                                      enum:
+                                        - Secret
+                                        - ConfigMap
+                                      type: string
+                                  required:
+                                    - name
+                                    - type
+                                  type: object
+                                certSecretRef:
+                                  description: |-
+                                    SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                                keySecretRef:
+                                  description: |-
+                                    SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                              required:
+                                - certSecretRef
+                                - keySecretRef
+                              type: object
+                            token:
+                              description: OvhClientToken defines the configuration required to authenticate to OVHcloud's Secret Manager using a token.
+                              properties:
+                                tokenSecretRef:
+                                  description: |-
+                                    SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                              required:
+                                - tokenSecretRef
+                              type: object
+                          type: object
+                        casRequired:
+                          description: 'Enables or disables check-and-set (CAS) (default: false).'
+                          type: boolean
+                        okmsTimeout:
+                          default: 30
+                          description: 'Setup a timeout in seconds when requests to the KMS are made (default: 30).'
+                          format: int32
+                          minimum: 1
+                          type: integer
+                        okmsid:
+                          description: specifies the OKMS ID.
+                          type: string
+                        server:
+                          description: specifies the OKMS server endpoint.
+                          type: string
+                      required:
+                        - auth
+                        - okmsid
+                        - server
+                      type: object
                     passbolt:
                       description: |-
                         PassboltProvider provides access to Passbolt secrets manager.

+ 236 - 0
docs/api/spec.md

@@ -1774,6 +1774,7 @@ string
 <a href="#external-secrets.io/v1.GitlabProvider">GitlabProvider</a>, 
 <a href="#external-secrets.io/v1.InfisicalProvider">InfisicalProvider</a>, 
 <a href="#external-secrets.io/v1.KubernetesServer">KubernetesServer</a>, 
+<a href="#external-secrets.io/v1.OvhClientMTLS">OvhClientMTLS</a>, 
 <a href="#external-secrets.io/v1.SecretServerProvider">SecretServerProvider</a>, 
 <a href="#external-secrets.io/v1.VaultProvider">VaultProvider</a>)
 </p>
@@ -8175,6 +8176,227 @@ External Secrets meta/v1.SecretKeySelector
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1.OvhAuth">OvhAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.OvhProvider">OvhProvider</a>)
+</p>
+<p>
+<p>OvhAuth tells the controller how to authenticate to OVHcloud&rsquo;s Secret Manager, either using mTLS or a token.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>mtls</code></br>
+<em>
+<a href="#external-secrets.io/v1.OvhClientMTLS">
+OvhClientMTLS
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+</td>
+</tr>
+<tr>
+<td>
+<code>token</code></br>
+<em>
+<a href="#external-secrets.io/v1.OvhClientToken">
+OvhClientToken
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1.OvhClientMTLS">OvhClientMTLS
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.OvhAuth">OvhAuth</a>)
+</p>
+<p>
+<p>OvhClientMTLS defines the configuration required to authenticate to OVHcloud&rsquo;s Secret Manager using mTLS.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>certSecretRef</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>
+</td>
+</tr>
+<tr>
+<td>
+<code>keySecretRef</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>
+</td>
+</tr>
+<tr>
+<td>
+<code>caBundle</code></br>
+<em>
+[]byte
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+</td>
+</tr>
+<tr>
+<td>
+<code>caProvider</code></br>
+<em>
+<a href="#external-secrets.io/v1.CAProvider">
+CAProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1.OvhClientToken">OvhClientToken
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.OvhAuth">OvhAuth</a>)
+</p>
+<p>
+<p>OvhClientToken defines the configuration required to authenticate to OVHcloud&rsquo;s Secret Manager using a token.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>tokenSecretRef</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>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1.OvhProvider">OvhProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>OvhProvider holds the configuration to synchronize secrets with OVHcloud&rsquo;s Secret Manager.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>server</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>specifies the OKMS server endpoint.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>okmsid</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>specifies the OKMS ID.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>casRequired</code></br>
+<em>
+bool
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Enables or disables check-and-set (CAS) (default: false).</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>okmsTimeout</code></br>
+<em>
+uint32
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Setup a timeout in seconds when requests to the KMS are made (default: 30).</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1.OvhAuth">
+OvhAuth
+</a>
+</em>
+</td>
+<td>
+<p>Authentication method (mtls or token).</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1.PassboltAuth">PassboltAuth
 </h3>
 <p>
@@ -9182,6 +9404,20 @@ VaultProvider
 </tr>
 <tr>
 <td>
+<code>ovh</code></br>
+<em>
+<a href="#external-secrets.io/v1.OvhProvider">
+OvhProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>OVHcloud configures this store to sync secrets using the OVHcloud provider.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>gcpsm</code></br>
 <em>
 <a href="#external-secrets.io/v1.GCPSMProvider">

+ 508 - 0
docs/provider/ovhcloud.md

@@ -0,0 +1,508 @@
+## Secrets Manager
+
+External Secrets Operator integrates with [OVHcloud KMS](https://www.ovhcloud.com/en/identity-security-operations/key-management-service/).  
+
+This guide demonstrates:
+- how to set up a `ClusterSecretStore`/`SecretStore` with the OVH provider.
+- `ExternalSecret` use cases with examples.
+- `PushSecret` use cases with examples.
+
+This guide assumes:
+- External Secrets Operator is already installed
+- You have access to OVHcloud Secret Manager
+- Required credentials are already created
+
+### <u>SecretStore</u>
+
+**OVH provider supports both `token` and `mTLS` authentication.**
+
+Token authentication:
+```yaml
+apiVersion: external-secrets.io/v1 
+kind: SecretStore
+metadata:
+  name: secret-store-ovh
+  namespace: default
+spec:
+  provider:
+    ovh:
+      server: <kms-endpoint>
+      okmsid: <okms-id>
+      auth:
+        token:
+          tokenSecretRef:
+            name: ovh-token
+            key: token
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: ovh-token
+data:
+  token: BASE64-TOKEN-VALUE-PLACEHOLDER
+```
+mTLS authentication:
+```yaml
+apiVersion: external-secrets.io/v1
+kind: SecretStore
+metadata:
+  name: secret-store-ovh
+  namespace: default
+spec:
+  provider:
+    ovh:
+      server: "https://eu-west-rbx.okms.ovh.net"
+      okmsid: "734b9b45-8b1a-469c-b140-b10bd6540017"
+      auth:
+        mtls:
+          certSecretRef:
+            name: ovh-mtls
+            key: tls.crt
+          keySecretRef:
+            name: ovh-mtls
+            key: tls.key
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: ovh-mtls
+  namespace: default
+type: kubernetes.io/tls
+data:
+  tls.crt: BASE64_CERT_PLACEHOLDER # "client certificate value"
+  tls.key: BASE64_KEY_PLACEHOLDER  # "client key value"
+```
+
+!!! note
+     A `ClusterSecretStore` configuration is the same except you must provide the `namespace` for `tokenSecretRef`, `certSecretRef` and `keySecretRef` according to your chosen authentication method.  
+
+### <u>ExternalSecret</u>
+ 
+For these examples, we will assume you have the following secret in your Secret Manager:
+```json
+{
+  "path": "creds",
+  "data": {
+    "type": "credential",
+    "users": {
+      "kevin": {
+        "token": "kevin token value"
+      },
+      "laura": {
+        "token": "laura token value"
+      }
+    }
+  }
+}
+```
+`path` refers to the secret's path in OVH Secret Manager.
+
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: external-secret-ovh
+  namespace: default
+spec:
+  secretStoreRef:
+    name: secret-store-ovh
+    kind: SecretStore
+  target:
+    name: secret-example
+  data:
+    - secretKey: foo
+      remoteRef:
+        key: creds
+        version: version
+        property: property
+```
+
+| Field      | Description                                                            | Required |
+|------------|------------------------------------------------------------------------|----------|
+| version    | Secret version to retrieve                                             | No       |
+| property   | Specific key or nested key in the secret                               | No       |
+| secretKey  | The key inside the Kubernetes Secret that will hold the secret's value | Yes      |
+
+#### Fetch the whole secret
+
+- Using `spec.data`
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: external-secret-ovh
+  namespace: default
+spec:
+  secretStoreRef:
+    name: secret-store-ovh
+    kind: SecretStore
+  target:
+    name: secret-example
+  data:
+    - secretKey: foo
+      remoteRef:
+        key: creds
+```
+Resulting Kubernetes Secret data:
+```json
+{
+  "foo": {
+    "type": "credential",
+    "users": {
+      "kevin": {
+        "token": "kevin token value"
+      },
+      "laura": {
+        "token": "laura token value"
+      }
+    }
+  }
+}
+```
+- Using `spec.dataFrom.extract`
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: external-secret-ovh
+  namespace: default
+spec:
+  secretStoreRef:
+    name: secret-store-ovh
+    kind: SecretStore
+  target:
+    name: secret-example
+  dataFrom:
+  - extract:
+      key: creds
+```
+Resulting Kubernetes Secret data:
+```json
+{
+  "type": "credential",
+  "users": {
+    "kevin": {
+      "token": "kevin token value"
+    },
+    "laura": {
+      "token": "laura token value"
+    }
+  }
+}
+```
+
+#### Fetch scalar/nested values
+- Scalar value using `data`
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: external-secret-ovh
+  namespace: default
+spec:
+  secretStoreRef:
+    name: secret-store-ovh
+    kind: SecretStore
+  target:
+    name: secret-example
+  data:
+    - secretKey: type
+      remoteRef:
+        key: creds
+        property: type
+```
+Resulting Kubernetes Secret data:
+```json
+{
+  "type": "credential"
+}
+```
+- Nested value using `data`
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: external-secret-ovh
+  namespace: default
+spec:
+  secretStoreRef:
+    name: secret-store-ovh
+    kind: SecretStore
+  target:
+    name: secret-example
+  data:
+    - secretKey: kevin-token
+      remoteRef:
+        key: creds
+        property: users.kevin.token
+```
+Resulting Kubernetes Secret data:
+```json
+{
+  "kevin-token": "kevin token value"
+}
+```
+- Nested value using `dataFrom.extract`
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: external-secret-ovh
+  namespace: default
+spec:
+  secretStoreRef:
+    name: secret-store-ovh
+    kind: SecretStore
+  target:
+    name: secret-example
+  dataFrom:
+  - extract:
+      key: creds
+      property: users
+```
+Resulting Kubernetes Secret data:
+```json
+{
+  "kevin": {
+    "token": "kevin token value"
+  },
+  "laura": {
+    "token": "laura token value"
+  }
+}
+```
+
+!!! warning
+     Scalar values cannot be retrieved using `dataFrom.extract` because no Kubernetes secret key can be specified, which would imply storing a value without a corresponding key.
+
+#### Fetch multiple secrets
+
+Extract multiple secrets, with filtering support.  
+You can filter either by path or/and regular expression. Path filtering occurs first if you use both.
+
+For these examples, we will assume you have the following secrets in your Secret Manager: `path/to/secret/secret1`, `path/to/secret/secret2`, `path/to/config/config2`, `path/to/config/config3`, `secret-example2`.
+- Path filtering
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: external-secret-ovh
+  namespace: default
+spec:
+  secretStoreRef:
+    name: secret-store-ovh
+    kind: SecretStore
+  target:
+    name: secret-example
+  dataFrom:
+  - find:
+      path: "path/to/secret"
+```
+Resulting Kubernetes Secret data:
+```json
+{
+  "path/to/secret/secret1": "secret1 value",
+  "path/to/secret/secret2": "secret2 value"
+}
+```
+!!! note
+     If path is left empty or is "/", every secret will be retrieved from your Secret Manager.
+
+- Regular expression filtering
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: external-secret-ovh
+  namespace: default
+spec:
+  secretStoreRef:
+    name: secret-store-ovh
+    kind: SecretStore
+  target:
+    name: secret-example
+  dataFrom:
+  - find:
+      name:
+        regexp: "[2-3]"
+```
+Resulting Kubernetes Secret data:
+```json
+{
+  "path/to/secret/secret2": "secret2 value",
+  "path/to/config/config2": "config2 value",
+  "path/to/config/config3": "config3 value",
+  "secret-example2": "secret-example2 value"
+}
+```
+!!! note
+     If name.regexp is left empty, every secret will be retrieved from your Secret Manager.
+
+- Combination of both
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: external-secret-ovh
+  namespace: default
+spec:
+  secretStoreRef:
+    name: secret-store-ovh
+    kind: SecretStore
+  target:
+    name: secret-example
+  dataFrom:
+  - find:
+      path: "path/to"
+      name:
+        regexp: "2$"
+```
+Resulting Kubernetes Secret data:
+```json
+{
+  "path/to/secret/secret2": "secret2 value",
+  "path/to/config/config2": "config2 value"
+}
+```
+
+!!! note
+     When both are combined, path filtering occurs first.
+
+### <u>PushSecret</u>
+
+#### Check-And-Set
+Check-And-Set can be enabled/disabled (default: disabled), in the Secret Store configuration:
+```yaml
+apiVersion: external-secrets.io/v1
+kind: SecretStore
+metadata:
+  name: secret-store-ovh
+  namespace: default
+spec:
+  provider:
+    ovh:
+      server: <kms-endpoint>
+      okmsid: <okms-id>
+      auth:
+        token:
+          tokenSecretRef:
+            name: ovh-token
+            key: token
+      casRequired: true
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: ovh-token
+data:
+  token: BASE64_TOKEN_PLACEHOLDER # "token value"
+```
+
+#### Secret Rotation
+```yaml
+apiVersion: generators.external-secrets.io/v1alpha1
+kind: Password
+metadata:
+  name: my-password-generator
+spec:
+  length: 32
+  digits: 5
+  symbols: 5
+  symbolCharacters: "-_^$%*ù/;:,?"
+  noUpper: false
+  allowRepeat: true
+---
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: push-secret-ovh
+spec:
+  refreshInterval: 6h0m0s
+  secretStoreRefs:
+    - name: secret-store-ovh
+      kind: SecretStore
+  selector:
+    generatorRef:
+      apiVersion: generators.external-secrets.io/v1alpha1
+      kind: Password
+      name: my-password-generator
+  data:
+    - match:
+        secretKey: password # property in the generator output
+        remoteRef:
+          remoteKey: prod/mysql/password
+```
+
+With this configuration, the secret is automatically rotated every 6 hours in the OVH Secret Manager.
+
+#### Secret migration
+```yaml
+apiVersion: external-secrets.io/v1
+kind: SecretStore
+metadata:
+  name: secret-store-vault
+  namespace: default
+spec:
+  provider:
+    vault:
+      server: "https://my.vault.server:8200"
+      path: "secret"
+      version: "v2"
+      auth:
+        tokenSecretRef:
+          name: vault-token
+          key: token
+---
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: external-secret-vault
+  namespace: default
+spec:
+  secretStoreRef:
+    name: secret-store-vault
+    kind: SecretStore
+  refreshPolicy: Periodic
+  refreshInterval: "10s"
+  target:
+    name: creds-secret-vault
+  dataFrom:
+    - extract:
+        key: example
+---
+apiVersion: external-secrets.io/v1
+kind: SecretStore
+metadata:
+  name: secret-store-ovh
+  namespace: default
+spec:
+  provider:
+    ovh:
+      server: <kms-endpoint>
+      okmsid: <okms-id>
+      auth:
+        token:
+          tokenSecretRef:
+            name: ovh-token
+            key: token
+---
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: push-secret-ovh
+spec:
+  secretStoreRefs:
+    - name: secret-store-ovh
+      kind: SecretStore
+  selector:
+    secret:
+      name: creds-secret-vault
+  refreshInterval: 10s
+  data:
+    - match:
+        secretKey: "secretKey"
+        remoteRef:
+          remoteKey: "creds-secret-migrated"
+```
+
+This example demonstrates how to fetch a secret from a HashiCorp Vault KV secrets engine and sync it into OVH Secret Manager.

+ 5 - 0
go.mod

@@ -46,6 +46,7 @@ replace (
 	github.com/external-secrets/external-secrets/providers/v1/onepassword => ./providers/v1/onepassword
 	github.com/external-secrets/external-secrets/providers/v1/onepasswordsdk => ./providers/v1/onepasswordsdk
 	github.com/external-secrets/external-secrets/providers/v1/oracle => ./providers/v1/oracle
+	github.com/external-secrets/external-secrets/providers/v1/ovh => ./providers/v1/ovh
 	github.com/external-secrets/external-secrets/providers/v1/passbolt => ./providers/v1/passbolt
 	github.com/external-secrets/external-secrets/providers/v1/passworddepot => ./providers/v1/passworddepot
 	github.com/external-secrets/external-secrets/providers/v1/previder => ./providers/v1/previder
@@ -159,6 +160,7 @@ require (
 	github.com/external-secrets/external-secrets/providers/v1/onepassword v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/onepasswordsdk v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/oracle v0.0.0-00010101000000-000000000000
+	github.com/external-secrets/external-secrets/providers/v1/ovh v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/passbolt v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/passworddepot v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/previder v0.0.0-00010101000000-000000000000
@@ -201,6 +203,7 @@ require (
 	github.com/ProtonMail/gopenpgp/v3 v3.3.0 // indirect
 	github.com/agext/levenshtein v1.2.3 // indirect
 	github.com/akeylesslabs/akeyless-go/v4 v4.3.0 // indirect
+	github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
 	github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
 	github.com/atotto/clipboard v0.1.4 // indirect
 	github.com/aws/aws-sdk-go-v2 v1.39.6 // indirect
@@ -317,7 +320,9 @@ require (
 	github.com/muesli/termenv v0.16.0 // indirect
 	github.com/nebius/gosdk v0.0.0-20260204094009-511fd4d4f7a1 // indirect
 	github.com/ngrok/ngrok-api-go/v7 v7.6.0 // indirect
+	github.com/oapi-codegen/runtime v1.1.2 // indirect
 	github.com/opentracing/basictracer-go v1.1.0 // indirect
+	github.com/ovh/okms-sdk-go v0.5.1 // indirect
 	github.com/passbolt/go-passbolt v0.8.0-beta.1 // indirect
 	github.com/pgavlin/fx v0.1.6 // indirect
 	github.com/pgavlin/fx/v2 v2.0.12 // indirect

+ 10 - 0
go.sum

@@ -164,6 +164,7 @@ github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5Qx
 github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
 github.com/ProtonMail/gopenpgp/v3 v3.3.0 h1:N6rHCH5PWwB6zSRMgRj1EbAMQHUAAHxH3Oo4KibsPwY=
 github.com/ProtonMail/gopenpgp/v3 v3.3.0/go.mod h1:J+iNPt0/5EO9wRt7Eit9dRUlzyu3hiGX3zId6iuaKOk=
+github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
 github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw=
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
@@ -189,6 +190,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
 github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
+github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
+github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
 github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
 github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@@ -259,6 +262,7 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
 github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
+github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
 github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+ArzlNzTtHydNyXUQ6zg=
 github.com/bradleyfalzon/ghinstallation/v2 v2.17.0/go.mod h1:vuD/xvJT9Y+ZVZRv4HQ42cMyPFIYqpc7AbB4Gvt/DlY=
 github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
@@ -768,6 +772,7 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
@@ -905,6 +910,8 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
 github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
 github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
+github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI=
+github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
 github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -931,6 +938,8 @@ github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mo
 github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
 github.com/oracle/oci-go-sdk/v65 v65.103.0 h1:HfyZx+JefCPK3At0Xt45q+wr914jDXuoyzOFX3XCbno=
 github.com/oracle/oci-go-sdk/v65 v65.103.0/go.mod h1:oB8jFGVc/7/zJ+DbleE8MzGHjhs2ioCz5stRTdZdIcY=
+github.com/ovh/okms-sdk-go v0.5.1 h1:oS8w/BXyGgnBzaGh3zFIyw73LwJ2B+UkNqeVBT2epUU=
+github.com/ovh/okms-sdk-go v0.5.1/go.mod h1:dJpK0dmnRphZgttfsjg6c5yLgcZp6LXPR/6CFwxY124=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/passbolt/go-passbolt v0.8.0-beta.1 h1:61EHrzrK9vC7uKQcVgyo5AyTj1CcFUgcJuXEOiYrIK8=
@@ -1058,6 +1067,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
 github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
 github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
 github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
 github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
 github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=

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

@@ -138,6 +138,7 @@ nav:
       - GitLab Variables: provider/gitlab-variables.md
       - Github Actions Secrets: provider/github.md
       - Oracle Vault: provider/oracle-vault.md
+      - OVHcloud: provider/ovhcloud.md
       - 1Password Connect Server: provider/1password-automation.md
       - 1Password SDK: provider/1password-sdk.md
       - Webhook: provider/webhook.md

+ 30 - 0
pkg/register/ovh.go

@@ -0,0 +1,30 @@
+//go:build ovh || all_providers
+
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 register provides explicit registration of all providers and generators.
+package register
+
+import (
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	ovh "github.com/external-secrets/external-secrets/providers/v1/ovh"
+)
+
+func init() {
+	// Register ovh provider
+	esv1.Register(ovh.NewProvider(), ovh.ProviderSpec(), ovh.MaintenanceStatus())
+}

+ 25 - 0
providers/v1/ovh/client_close.go

@@ -0,0 +1,25 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+)
+
+func (cl *ovhClient) Close(_ context.Context) error {
+	return nil
+}

+ 42 - 0
providers/v1/ovh/client_delete_secret.go

@@ -0,0 +1,42 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+// If deletionPolicy is set to Delete, the Secret Manager Secret
+// created from the Push Secret will be automatically removed
+// when the associated Push Secret is deleted.
+func (cl *ovhClient) DeleteSecret(ctx context.Context, remoteRef esv1.PushSecretRemoteRef) error {
+	err := cl.okmsClient.DeleteSecretV2(ctx, cl.okmsID, remoteRef.GetRemoteKey())
+
+	if err != nil {
+		err = handleOkmsError(err)
+		if errors.Is(err, esv1.NoSecretErr) {
+			return nil
+		}
+		return fmt.Errorf("failed to delete secret at path %q: %w", remoteRef.GetRemoteKey(), err)
+	}
+
+	return nil
+}

+ 208 - 0
providers/v1/ovh/client_get_all_secrets.go

@@ -0,0 +1,208 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	ppath "path"
+	"regexp"
+	"strings"
+
+	"github.com/google/uuid"
+	"github.com/ovh/okms-sdk-go/types"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+const retrieveMultipleSecretsError = "failed to retrieve multiple secrets"
+
+// GetAllSecrets retrieves multiple secrets from the Secret Manager.
+// You can optionally filter secrets by name using a regular expression.
+// When path is set to "" or left empty, the search starts from the Secret Manager root.
+func (cl *ovhClient) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
+	// List Secret Manager secrets.
+	secrets, err := getSecretsList(ctx, cl.okmsClient, cl.okmsID, ref.Path)
+	if err != nil {
+		return map[string][]byte{}, fmt.Errorf("%s: %w", retrieveMultipleSecretsError, err)
+	}
+	if len(secrets) == 0 {
+		return map[string][]byte{}, nil
+	}
+
+	// Compile the regular expression defined in ref.Name.RegExp, if present.
+	var regex *regexp.Regexp
+
+	if ref.Name != nil {
+		regex, err = regexp.Compile(ref.Name.RegExp)
+		if err != nil {
+			return map[string][]byte{}, fmt.Errorf(
+				"%s: could not parse regex: %w",
+				retrieveMultipleSecretsError,
+				err,
+			)
+		}
+		if regex == nil {
+			return map[string][]byte{}, fmt.Errorf(
+				"%s: compiled regex is nil for expression %q",
+				retrieveMultipleSecretsError,
+				ref.Name.RegExp,
+			)
+		}
+	}
+
+	secretsMap, err := filterSecretsListWithRegexp(ctx, cl, secrets, regex)
+	if err != nil {
+		return map[string][]byte{}, fmt.Errorf("%s: %w", retrieveMultipleSecretsError, err)
+	}
+
+	return secretsMap, nil
+}
+
+// Retrieve secrets located under the specified path.
+// If the path is omitted, all secrets from the Secret Manager are returned.
+func getSecretsList(ctx context.Context, okmsClient OkmsClient, okmsID uuid.UUID, path *string) ([]string, error) {
+	// Ignore invalid path
+	if path != nil && (strings.HasPrefix(*path, "/") || strings.Contains(*path, "//")) {
+		return []string{}, fmt.Errorf("invalid path %q: cannot start with a / or contain a //", *path)
+	}
+
+	formatPath := ""
+	if path != nil && *path != "" {
+		formatPath = *path
+	}
+
+	// Ensure `formatPath` does not end with '/', otherwise, GetSecretsMetadata
+	// will not be able to retrieve secrets as it should.
+	formatPath = strings.TrimSuffix(formatPath, "/")
+
+	return recursivelyGetSecretsList(ctx, okmsClient, okmsID, formatPath)
+}
+
+// Recursively traverses the path to retrieve all secrets it contains.
+//
+// The recursion stops when the for loop finishes iterating over the list
+// returned by GetSecretsMetadata, or when an error occurs.
+//
+// A recursive call is triggered whenever a key ends with '/'.
+//
+// Example:
+// Given the secrets ["secret1", "path/secret", "path/to/secret"] stored in the
+// Secret Manager, an initial call to recursivelyGetSecretsList with path="path"
+// will cause GetSecretsMetadata to return ["secret", "to/"]
+// (see Note below for details on this behavior).
+//
+// - "secret" is added to the local secret list.
+// - "to/" triggers a recursive call with path="path/to".
+//
+// In the second call, GetSecretsMetadata returns ["secret"], which is added to
+// the local list. Since no key ends with '/', the recursion stops and the list
+// is returned and merged into the result of the first call.
+//
+// Note: OVH's SDK GetSecretsMetadata does not return full paths.
+// It returns only the next element of the hierarchy, and adds a trailing '/'
+// when the element is a directory (i.e., not the last component).
+//
+// Examples:
+//
+//	secret1 = "path/to/secret1"
+//	secret2 = "path/secret2"
+//	secret3 = "path/secrets/secret3"
+//
+// For the path "path", GetSecretsMetadata returns:
+//
+//	["to/", "secret2", "secrets/"]
+func recursivelyGetSecretsList(ctx context.Context, okmsClient OkmsClient, okmsID uuid.UUID, path string) ([]string, error) {
+	// Retrieve the list of KMS secrets for the given path.
+	// If no path is provided, retrieve all existing secrets from KMS.
+	secrets, err := okmsClient.GetSecretsMetadata(ctx, okmsID, path, true)
+	if err != nil {
+		return nil, fmt.Errorf("could not list secrets at path %q: %w", path, err)
+	}
+	if secrets == nil || secrets.Data == nil || secrets.Data.Keys == nil || len(*secrets.Data.Keys) == 0 {
+		return nil, nil
+	}
+
+	return secretListLoop(ctx, secrets, okmsClient, okmsID, path)
+}
+
+// Loop over each key under 'path'.
+// If a key represents a directory (ends with '/')
+// and is valid (does not begin with '/' and does not contain successive '/'),
+// a recursive call is made.
+// Otherwise, the key is a secret and is added to the result list.
+func secretListLoop(ctx context.Context, secrets *types.GetMetadataResponse, okmsClient OkmsClient, okmsID uuid.UUID, path string) ([]string, error) {
+	secretsList := make([]string, 0, len(*secrets.Data.Keys))
+
+	for _, key := range *secrets.Data.Keys {
+		if key == "" || strings.HasPrefix(key, "/") {
+			continue
+		}
+
+		if before, ok := strings.CutSuffix(key, "/"); ok {
+			toAppend, err := recursivelyGetSecretsList(ctx, okmsClient, okmsID, ppath.Join(path, before))
+			if err != nil {
+				return nil, err
+			}
+			secretsList = append(secretsList, toAppend...)
+			continue
+		}
+		secretsList = append(secretsList, ppath.Join(path, key))
+	}
+
+	return secretsList, nil
+}
+
+// Filter the list of secrets using a regular expression.
+func filterSecretsListWithRegexp(ctx context.Context, cl *ovhClient, secrets []string, regex *regexp.Regexp) (map[string][]byte, error) {
+	secretsDataMap := make(map[string][]byte)
+	for _, secret := range secrets {
+		// Insert the secret if no regex is provided;
+		// otherwise, insert only matching secrets.
+		secretData, ok, err := fetchSecretData(ctx, cl, secret, regex)
+		if err != nil {
+			return map[string][]byte{}, err
+		}
+		if ok {
+			secretsDataMap[secret] = secretData
+		}
+	}
+
+	return secretsDataMap, nil
+}
+
+// fetchSecretData retrieves a secret data if it passes the name/regex filter.
+func fetchSecretData(ctx context.Context, cl *ovhClient, secret string, regex *regexp.Regexp) ([]byte, bool, error) {
+	// Skip the secret if a name filter is defined but the regex is nil or does not match.
+	if regex != nil && !regex.MatchString(secret) {
+		return nil, false, nil
+	}
+
+	// fetch secret data
+	secretData, err := cl.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
+		Key: secret,
+	})
+	if err != nil {
+		if errors.Is(err, esv1.NoSecretErr) {
+			return nil, false, nil
+		}
+		return nil, false, err
+	}
+
+	return secretData, true, nil
+}

+ 169 - 0
providers/v1/ovh/client_get_all_secrets_test.go

@@ -0,0 +1,169 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"testing"
+
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	"github.com/external-secrets/external-secrets/providers/v1/ovh/fake"
+)
+
+func TestGetAllSecrets(t *testing.T) {
+	path2 := "pattern2/test"
+	slashPath := "pattern//slash"
+	emptySecretPath := "empty"
+	nilSecretPath := "nil"
+
+	noMatchRegexp := "^noMatch.*$"
+	invalidRegexp := "\\wa\\w([a]"
+
+	testCases := map[string]struct {
+		should     map[string][]byte
+		errshould  string
+		kube       kclient.Client
+		refFind    esv1.ExternalSecretFind
+		okmsClient fake.FakeOkmsClient
+	}{
+		"Empty Secret Found": {
+			errshould: fmt.Sprintf("failed to retrieve multiple secrets: failed to retrieve secret at path %q: secret version data is missing", emptySecretPath+"/empty-secret"),
+			refFind: esv1.ExternalSecretFind{
+				Path: &emptySecretPath,
+			},
+		},
+		"Nil Secret Found": {
+			errshould: fmt.Sprintf("failed to retrieve multiple secrets: failed to retrieve secret at path %q: secret version data is missing", nilSecretPath+"/nil-secret"),
+			refFind: esv1.ExternalSecretFind{
+				Path: &nilSecretPath,
+			},
+		},
+		"Invalid Regex": {
+			errshould: fmt.Sprintf("failed to retrieve multiple secrets: could not parse regex: error parsing regexp: missing closing ): `%s`", invalidRegexp),
+			refFind: esv1.ExternalSecretFind{
+				Name: &esv1.FindName{
+					RegExp: invalidRegexp,
+				},
+			},
+		},
+		"Empty Regex": {
+			should: map[string][]byte{
+				"mysecret":                  []byte(`{"key1":"value1","key2":"value2"}`),
+				"nested-secret":             []byte(`{"users":{"alice":{"age":"23"},"baptist":{"age":"27"}}}`),
+				"pattern2/test/test-secret": []byte("{\"key4\":\"value4\"}"),
+				"pattern2/test/test.secret": []byte("{\"key5\":\"value5\"}"),
+				"pattern2/secret":           []byte("{\"key6\":\"value6\"}"),
+			},
+			refFind: esv1.ExternalSecretFind{
+				Name: &esv1.FindName{
+					RegExp: "",
+				},
+			},
+		},
+		"No Regexp Match": {
+			refFind: esv1.ExternalSecretFind{
+				Name: &esv1.FindName{
+					RegExp: noMatchRegexp,
+				},
+			},
+			should: map[string][]byte{},
+		},
+		"Regex pattern containing '.' or '-' only": {
+			should: map[string][]byte{
+				"nested-secret":             []byte(`{"users":{"alice":{"age":"23"},"baptist":{"age":"27"}}}`),
+				"pattern2/test/test-secret": []byte("{\"key4\":\"value4\"}"),
+				"pattern2/test/test.secret": []byte("{\"key5\":\"value5\"}"),
+			},
+			refFind: esv1.ExternalSecretFind{
+				Name: &esv1.FindName{
+					RegExp: ".*[.-].*",
+				},
+			},
+		},
+		"Path pattern2/test": {
+			should: map[string][]byte{
+				"pattern2/test/test-secret": []byte("{\"key4\":\"value4\"}"),
+				"pattern2/test/test.secret": []byte("{\"key5\":\"value5\"}"),
+			},
+			refFind: esv1.ExternalSecretFind{
+				Path: &path2,
+			},
+		},
+		"Path pattern//wrong": {
+			should: map[string][]byte{
+				"/pattern2/test/testsecret":  []byte("{\"key4\":\"value4\"}"),
+				"pattern2/test/test//secret": []byte("{\"key5\":\"value5\"}"),
+			},
+			refFind: esv1.ExternalSecretFind{
+				Path: &slashPath,
+			},
+			errshould: "failed to retrieve multiple secrets: invalid path \"pattern//slash\": cannot start with a / or contain a //",
+		},
+		"Secrets found without path": {
+			should: map[string][]byte{
+				"mysecret":                  []byte(`{"key1":"value1","key2":"value2"}`),
+				"nested-secret":             []byte(`{"users":{"alice":{"age":"23"},"baptist":{"age":"27"}}}`),
+				"pattern2/test/test-secret": []byte("{\"key4\":\"value4\"}"),
+				"pattern2/test/test.secret": []byte("{\"key5\":\"value5\"}"),
+				"pattern2/secret":           []byte("{\"key6\":\"value6\"}"),
+			},
+			refFind: esv1.ExternalSecretFind{
+				Path: nil,
+			},
+		},
+	}
+
+	ctx := context.Background()
+	for name, testCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			cl := &ovhClient{
+				okmsClient: testCase.okmsClient,
+				kube:       testCase.kube,
+			}
+			secrets, err := cl.GetAllSecrets(ctx, testCase.refFind)
+
+			if testCase.errshould != "" {
+				if err == nil {
+					t.Errorf("\nexpected value: %s\nactual value:   <nil>\n\n", testCase.errshould)
+				} else if err.Error() != testCase.errshould {
+					t.Errorf("\nexpected value: %s\nactual value:   %v\n\n", testCase.errshould, err)
+				}
+				return
+			} else if err != nil {
+				t.Errorf("\nunexpected error: %v\n\n", err)
+				return
+			}
+			if !reflect.DeepEqual(testCase.should, secrets) {
+				t.Errorf("\nexpected value: %v\nactual value:   %v\n\n", convertByteMapToStringMap(testCase.should), convertByteMapToStringMap(secrets))
+			}
+		})
+	}
+}
+
+func convertByteMapToStringMap(m map[string][]byte) map[string]string {
+	newMap := make(map[string]string)
+
+	for key, value := range m {
+		newMap[key] = string(value)
+	}
+
+	return newMap
+}

+ 41 - 0
providers/v1/ovh/client_get_secret.go

@@ -0,0 +1,41 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+// GetSecret retrieves a single secret from the provider.
+// The created secret will store the entire secret value under the specified key.
+// You can specify a key, a property and a version.
+func (cl *ovhClient) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	// Retrieve the KMS secret using the OVH SDK.
+	secretData, _, err := cl.getSecretWithOvhSDK(ctx, cl.okmsID, ref)
+	if err != nil {
+		if errors.Is(err, esv1.NoSecretErr) {
+			return []byte{}, err
+		}
+		return []byte{}, fmt.Errorf("failed to retrieve secret at path %q: %w", ref.Key, err)
+	}
+
+	return secretData, nil
+}

+ 70 - 0
providers/v1/ovh/client_get_secret_map.go

@@ -0,0 +1,70 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	"github.com/external-secrets/external-secrets/runtime/esutils"
+)
+
+const retrieveSecretError = "failed to retrieve secret at path"
+
+// GetSecretMap retrieves a single secret from the provider.
+// The created secret will have the same keys as the Secret Manager secret.
+// You can specify a key, a property, and a version.
+// If a property is provided, it should reference only nested values.
+func (cl *ovhClient) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	// Retrieve secret from KMS.
+	secretDataBytes, _, err := cl.getSecretWithOvhSDK(ctx, cl.okmsID, ref)
+	if err != nil {
+		if errors.Is(err, esv1.NoSecretErr) {
+			return map[string][]byte{}, err
+		}
+		return map[string][]byte{}, wrapRetrieveSecretError(ref.Key, err)
+	}
+	if len(secretDataBytes) == 0 {
+		return map[string][]byte{}, nil
+	}
+
+	// Unmarshal the secret value into a map[string]any
+	// so it can be passed to esutils.GetByteValueFromMap.
+	var rawSecretDataMap map[string]any
+	err = json.Unmarshal(secretDataBytes, &rawSecretDataMap)
+	if err != nil {
+		return map[string][]byte{}, wrapRetrieveSecretError(ref.Key, err)
+	}
+
+	// Convert the map[string]any into map[string][]byte.
+	secretDataMap := make(map[string][]byte, len(rawSecretDataMap))
+	for key := range rawSecretDataMap {
+		secretDataMap[key], err = esutils.GetByteValueFromMap(rawSecretDataMap, key)
+		if err != nil {
+			return map[string][]byte{}, wrapRetrieveSecretError(ref.Key, err)
+		}
+	}
+
+	return secretDataMap, nil
+}
+
+func wrapRetrieveSecretError(key string, err error) error {
+	return fmt.Errorf("%s %q: %w", retrieveSecretError, key, err)
+}

+ 143 - 0
providers/v1/ovh/client_get_secret_map_test.go

@@ -0,0 +1,143 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"reflect"
+	"testing"
+
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	"github.com/external-secrets/external-secrets/providers/v1/ovh/fake"
+)
+
+func TestGetSecretMap(t *testing.T) {
+	mySecretRemoteKey := "mysecret"
+	myNestedSecretRemoteKey := "nested-secret"
+	nonExistentSecretRemoteKey := "non-existent-secret"
+	emptySecretRemoteKey := "empty-secret"
+	nilSecretRemoteKey := "nil-secret"
+
+	nestedProperty := "users.alice"
+	scalarValueProperty := "users.alice.age"
+	invalidProperty := "invalid-property"
+
+	testCases := map[string]struct {
+		should     map[string][]byte
+		errshould  string
+		kube       kclient.Client
+		okmsClient fake.FakeOkmsClient
+		ref        esv1.ExternalSecretDataRemoteRef
+	}{
+		"Valid Secret": {
+			should: map[string][]byte{
+				"key1": []byte("value1"),
+				"key2": []byte("value2"),
+			},
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key: mySecretRemoteKey,
+			},
+		},
+		"Non-existent Secret": {
+			errshould: "failed to parse the following okms error: Secret does not exist",
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key: nonExistentSecretRemoteKey,
+			},
+		},
+		"Secret with nil data": {
+			errshould: fmt.Sprintf("failed to retrieve secret at path %q: secret version data is missing", nilSecretRemoteKey),
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key: nilSecretRemoteKey,
+			},
+		},
+		"Secret without empty data": {
+			errshould: fmt.Sprintf("failed to retrieve secret at path %q: secret version data is missing", emptySecretRemoteKey),
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key: emptySecretRemoteKey,
+			},
+		},
+		"Fetch MetaDataPolicy": {
+			errshould: fmt.Sprintf("failed to retrieve secret at path %q: fetch metadata policy not supported", mySecretRemoteKey),
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key:            mySecretRemoteKey,
+				MetadataPolicy: "Fetch",
+			},
+		},
+		"Property": {
+			should: map[string][]byte{
+				"age": []byte("23"),
+			},
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key:      myNestedSecretRemoteKey,
+				Property: nestedProperty,
+			},
+		},
+		"Scalar Value Property": {
+			errshould: fmt.Sprintf("failed to retrieve secret at path %q: json: cannot unmarshal number into Go value of type map[string]interface {}", myNestedSecretRemoteKey),
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key:      myNestedSecretRemoteKey,
+				Property: scalarValueProperty,
+			},
+		},
+		"Invalid Property": {
+			errshould: fmt.Sprintf("failed to retrieve secret at path %q: secret property %q not found", mySecretRemoteKey, invalidProperty),
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key:      mySecretRemoteKey,
+				Property: invalidProperty,
+			},
+		},
+		"Error case": {
+			errshould: fmt.Sprintf("failed to retrieve secret at path %q: failed to parse the following okms error: custom error", mySecretRemoteKey),
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key:      mySecretRemoteKey,
+				Property: invalidProperty,
+			},
+			okmsClient: fake.FakeOkmsClient{
+				GetSecretV2Fn: fake.NewGetSecretV2Fn(mySecretRemoteKey, errors.New("custom error")),
+			},
+		},
+	}
+
+	for name, testCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			ctx := context.Background()
+			cl := &ovhClient{
+				okmsClient: testCase.okmsClient,
+				kube:       testCase.kube,
+			}
+			secret, err := cl.GetSecretMap(ctx, testCase.ref)
+			if testCase.errshould != "" {
+				if err == nil {
+					t.Errorf("\nexpected value: %s\nactual value:   <nil>\n\n", testCase.errshould)
+				} else if err.Error() != testCase.errshould {
+					t.Errorf("\nexpected value: %s\nactual value:   %v\n\n", testCase.errshould, err)
+				}
+				return
+			} else if err != nil {
+				t.Errorf("\nunexpected error: %v\n\n", err)
+				return
+			}
+			if !reflect.DeepEqual(testCase.should, secret) {
+				t.Errorf("\nexpected value: %v\nactual value:   %v\n\n", convertByteMapToStringMap(testCase.should), convertByteMapToStringMap(secret))
+			}
+		})
+	}
+}

+ 136 - 0
providers/v1/ovh/client_get_secret_test.go

@@ -0,0 +1,136 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"testing"
+
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	"github.com/external-secrets/external-secrets/providers/v1/ovh/fake"
+)
+
+func TestGetSecret(t *testing.T) {
+	mySecretRemoteKey := "mysecret"
+	myNestedSecretRemoteKey := "nested-secret"
+	nonExistentSecretRemoteKey := "non-existent-secret"
+	emptySecretRemoteKey := "empty-secret"
+	nilSecretRemoteKey := "nil-secret"
+
+	property := "key1"
+	nestedProperty := "users.alice.age"
+	invalidProperty := "invalid-property"
+
+	testCases := map[string]struct {
+		should     string
+		errshould  string
+		kube       kclient.Client
+		ref        esv1.ExternalSecretDataRemoteRef
+		okmsClient fake.FakeOkmsClient
+	}{
+		"Valid Secret": {
+			should: "{\"key1\":\"value1\",\"key2\":\"value2\"}",
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key: mySecretRemoteKey,
+			},
+		},
+		"Non-existent Secret": {
+			errshould: "failed to parse the following okms error: Secret does not exist",
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key: nonExistentSecretRemoteKey,
+			},
+		},
+		"Secret with nil data": {
+			errshould: fmt.Sprintf("failed to retrieve secret at path %q: secret version data is missing", nilSecretRemoteKey),
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key: nilSecretRemoteKey,
+			},
+		},
+		"Secret without empty data": {
+			errshould: fmt.Sprintf("failed to retrieve secret at path %q: secret version data is missing", emptySecretRemoteKey),
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key: emptySecretRemoteKey,
+			},
+		},
+		"Fetch MetaDataPolicy": {
+			errshould: fmt.Sprintf("failed to retrieve secret at path %q: fetch metadata policy not supported", mySecretRemoteKey),
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key:            mySecretRemoteKey,
+				MetadataPolicy: "Fetch",
+			},
+		},
+		"Property": {
+			should: "value1",
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key:      mySecretRemoteKey,
+				Property: property,
+			},
+		},
+		"Nested Property": {
+			should: "23",
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key:      myNestedSecretRemoteKey,
+				Property: nestedProperty,
+			},
+		},
+		"Invalid Property": {
+			errshould: fmt.Sprintf("failed to retrieve secret at path %q: secret property %q not found", mySecretRemoteKey, invalidProperty),
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key:      mySecretRemoteKey,
+				Property: invalidProperty,
+			},
+		},
+		"Error case": {
+			errshould: fmt.Sprintf("failed to retrieve secret at path %q: failed to parse the following okms error: custom error", mySecretRemoteKey),
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key:      mySecretRemoteKey,
+				Property: invalidProperty,
+			},
+			okmsClient: fake.FakeOkmsClient{
+				GetSecretV2Fn: fake.NewGetSecretV2Fn(mySecretRemoteKey, errors.New("custom error")),
+			},
+		},
+	}
+	for name, testCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			ctx := context.Background()
+			cl := &ovhClient{
+				okmsClient: testCase.okmsClient,
+				kube:       testCase.kube,
+			}
+			secret, err := cl.GetSecret(ctx, testCase.ref)
+			if testCase.errshould != "" {
+				if err == nil {
+					t.Errorf("\nexpected error: %s\nactual error:   <nil>\n\n", testCase.errshould)
+				} else if err.Error() != testCase.errshould {
+					t.Errorf("\nexpected error: %s\nactual error:   %v\n\n", testCase.errshould, err)
+				}
+				return
+			} else if err != nil {
+				t.Errorf("\nunexpected error: %v\n\n", err)
+				return
+			}
+			if testCase.should != "" && string(secret) != testCase.should {
+				t.Errorf("\nexpected value: %q\nactual value:   %q\n\n", testCase.should, string(secret))
+			}
+		})
+	}
+}

+ 210 - 0
providers/v1/ovh/client_push_secret.go

@@ -0,0 +1,210 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"reflect"
+
+	"github.com/google/uuid"
+	"github.com/ovh/okms-sdk-go/types"
+	corev1 "k8s.io/api/core/v1"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+const pushSecretError = "failed to push secret at path"
+
+// PushSecret pushes a secret to the Secret Manager according to the updatePolicy
+// defined in the PushSecret (create or update).
+func (cl *ovhClient) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
+	remoteKey := data.GetRemoteKey()
+
+	if secret == nil {
+		return newPushSecretValidationError(remoteKey, "provided secret is nil")
+	}
+	if len(secret.Data) == 0 {
+		return newPushSecretValidationError(remoteKey, "provided secret is empty")
+	}
+
+	// Check if the secret already exists.
+	// This determines which method to use: create or update.
+	remoteSecret, currentVersion, err := cl.getSecretWithOvhSDK(ctx, cl.okmsID, esv1.ExternalSecretDataRemoteRef{
+		Key: remoteKey,
+	})
+	noSecretErr := errors.Is(err, esv1.NoSecretErr)
+	if err != nil && !noSecretErr {
+		return wrapPushSecretError(remoteKey, err)
+	}
+	secretExists := !noSecretErr
+
+	// Build the secret to be pushed.
+	secretToPush, err := buildSecretToPush(secret, data)
+	if err != nil {
+		return wrapPushSecretError(remoteKey, err)
+	}
+
+	// Compare the data of secretToPush with that of remoteSecret.
+	equal, err := compareSecretsData(secretToPush, remoteSecret)
+	if err != nil {
+		return wrapPushSecretError(remoteKey, err)
+	}
+	if equal {
+		return nil
+	}
+
+	// Set cas according to client configuration
+	if !cl.cas {
+		currentVersion = nil
+	}
+
+	// Push the secret.
+	err = pushNewSecret(ctx, cl.okmsClient, cl.okmsID, secretToPush, remoteKey, currentVersion, secretExists)
+	if err != nil {
+		return wrapPushSecretError(remoteKey, err)
+	}
+	return nil
+}
+
+func wrapPushSecretError(remoteKey string, err error) error {
+	return fmt.Errorf("%s %q: %w", pushSecretError, remoteKey, err)
+}
+
+func newPushSecretValidationError(remoteKey, msg string) error {
+	return fmt.Errorf("%s %q: %s", pushSecretError, remoteKey, msg)
+}
+
+// Compare the secret to push with the remote secret.
+// If they are equal, do not push the secret.
+func compareSecretsData(secretToPush map[string]any, remoteSecret []byte) (bool, error) {
+	if len(remoteSecret) == 0 {
+		return false, nil
+	}
+
+	localBytes, err := json.Marshal(secretToPush)
+	if err != nil {
+		return false, fmt.Errorf("could not compare remote secret with secret to push: %w", err)
+	}
+
+	var localSecretMap, remoteSecretMap any
+	if err := json.Unmarshal(localBytes, &localSecretMap); err != nil {
+		return false, fmt.Errorf("could not normalize local secret for comparison: %w", err)
+	}
+	if err := json.Unmarshal(remoteSecret, &remoteSecretMap); err != nil {
+		return false, fmt.Errorf("could not normalize remote secret for comparison: %w", err)
+	}
+
+	return reflect.DeepEqual(localSecretMap, remoteSecretMap), nil
+}
+
+// Build the secret to be pushed.
+//
+// If remoteProperty is defined, it will be used as the key to store the secret value.
+// If secretKey is not defined, the entire secret value will be pushed.
+// Otherwise, only the value of the specified secretKey will be pushed.
+func buildSecretToPush(secret *corev1.Secret, data esv1.PushSecretData) (map[string]any, error) {
+	// Retrieve the secret value to push based on secretKey.
+	var secretValueToPush map[string]any
+	var err error
+
+	secretValueToPush, err = extractSecretValue(secret.Data, data.GetSecretKey())
+
+	if err != nil {
+		return map[string]any{}, err
+	}
+
+	// Build the secret to push using remoteProperty.
+	secretToPush := make(map[string]any)
+	property := data.GetProperty()
+
+	if property == "" {
+		secretToPush = secretValueToPush
+	} else {
+		secretToPush[property] = secretValueToPush
+	}
+
+	return secretToPush, nil
+}
+
+func extractSecretValue(data map[string][]byte, secretKey string) (map[string]any, error) {
+	var err error
+	secretValueToPush := make(map[string]any)
+
+	if secretKey != "" {
+		err = extractSecretKeyValue(data, secretValueToPush, secretKey)
+		return secretValueToPush, err
+	}
+
+	for key := range data {
+		err = extractSecretKeyValue(data, secretValueToPush, key)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return secretValueToPush, nil
+}
+
+func extractSecretKeyValue(data map[string][]byte, secretValueToPush map[string]any, secretKey string) error {
+	value, ok := data[secretKey]
+	if !ok {
+		return fmt.Errorf(
+			"could not extract secret key value to push: secretKey %q not found in secret data", secretKey,
+		)
+	}
+	var decoded any
+	if json.Unmarshal(value, &decoded) != nil {
+		secretValueToPush[secretKey] = string(value)
+	} else {
+		secretValueToPush[secretKey] = json.RawMessage(value)
+	}
+
+	return nil
+}
+
+// This pushes the created/updated secret.
+func pushNewSecret(ctx context.Context, okmsClient OkmsClient, okmsID uuid.UUID, secretToPush map[string]any, path string, cas *uint32, secretExists bool) error {
+	var err error
+
+	if !secretExists {
+		// Create a secret.
+		_, err = okmsClient.PostSecretV2(ctx, okmsID, types.PostSecretV2Request{
+			Path: path,
+			Version: types.SecretV2VersionShort{
+				Data: &secretToPush,
+			},
+		})
+		if err != nil {
+			return fmt.Errorf("could not create remote secret: %w", err)
+		}
+		return nil
+	}
+
+	// Update a secret.
+	_, err = okmsClient.PutSecretV2(ctx, okmsID, path, cas, types.PutSecretV2Request{
+		Version: &types.SecretV2VersionShort{
+			Data: &secretToPush,
+		},
+	})
+	if err != nil {
+		return fmt.Errorf("could not update remote secret: %w", err)
+	}
+	return nil
+}

+ 159 - 0
providers/v1/ovh/client_push_secret_test.go

@@ -0,0 +1,159 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"testing"
+
+	v1 "k8s.io/api/core/v1"
+
+	"github.com/external-secrets/external-secrets/providers/v1/ovh/fake"
+	testingfake "github.com/external-secrets/external-secrets/runtime/testing/fake"
+)
+
+func TestPushSecret(t *testing.T) {
+	const (
+		mySecretRemoteKey          = "mysecret"
+		nonExistentSecretRemoteKey = "non-existent-secret"
+		emptySecretRemoteKey       = "empty-secret"
+		nilSecretRemoteKey         = "nil-secret"
+	)
+	secretData := &v1.Secret{
+		Data: map[string][]byte{
+			"key1": []byte("value1"),
+			"key2": []byte("value2"),
+		},
+	}
+	emptyRemoteKey := ""
+
+	testCases := map[string]struct {
+		errshould  string
+		secret     *v1.Secret
+		data       testingfake.PushSecretData
+		okmsClient fake.FakeOkmsClient
+	}{
+		"Nil Secret": {
+			errshould: fmt.Sprintf("failed to push secret at path %q: provided secret is nil", nilSecretRemoteKey),
+			secret:    nil,
+			data: testingfake.PushSecretData{
+				RemoteKey: nilSecretRemoteKey,
+			},
+		},
+		"Empty Secret Data": {
+			errshould: fmt.Sprintf("failed to push secret at path %q: provided secret is empty", emptySecretRemoteKey),
+			secret: &v1.Secret{
+				Data: nil,
+			},
+			data: testingfake.PushSecretData{
+				RemoteKey: emptySecretRemoteKey,
+			},
+		},
+		"Empty Remote Key": {
+			errshould: fmt.Sprintf("failed to push secret at path %q: remote key cannot be empty (spec.data.remoteRef.key)", emptyRemoteKey),
+			secret:    secretData,
+			data: testingfake.PushSecretData{
+				RemoteKey: emptyRemoteKey,
+			},
+		},
+		"Non-Existent Remote Key": {
+			errshould: "",
+			secret:    secretData,
+			data: testingfake.PushSecretData{
+				RemoteKey: nonExistentSecretRemoteKey,
+			},
+		},
+		"Existing Remote Key": {
+			errshould: "",
+			secret:    secretData,
+			data: testingfake.PushSecretData{
+				RemoteKey: mySecretRemoteKey,
+			},
+		},
+		"Secret Key": {
+			errshould: "",
+			secret:    secretData,
+			data: testingfake.PushSecretData{
+				RemoteKey: mySecretRemoteKey,
+				SecretKey: "key1",
+			},
+		},
+		"Property": {
+			errshould: "",
+			secret:    secretData,
+			data: testingfake.PushSecretData{
+				RemoteKey: mySecretRemoteKey,
+				Property:  "property",
+			},
+		},
+		"Custom PostSecretV2 Error": {
+			errshould: fmt.Sprintf("failed to push secret at path %q: could not create remote secret: custom error", mySecretRemoteKey),
+			secret:    secretData,
+			data: testingfake.PushSecretData{
+				RemoteKey: mySecretRemoteKey,
+			},
+			okmsClient: fake.FakeOkmsClient{
+				// A non-existent secret is referenced to trigger Post instead of Put
+				GetSecretV2Fn:  fake.NewGetSecretV2Fn(nonExistentSecretRemoteKey, nil),
+				PostSecretV2Fn: fake.NewPostSecretV2Fn(errors.New("custom error")),
+			},
+		},
+		"Custom PutSecretV2 Error": {
+			errshould: fmt.Sprintf("failed to push secret at path %q: could not update remote secret: custom error", mySecretRemoteKey),
+			secret:    secretData,
+			data: testingfake.PushSecretData{
+				RemoteKey: mySecretRemoteKey,
+			},
+			okmsClient: fake.FakeOkmsClient{
+				// An existing secret is referenced to trigger Put instead of Post
+				GetSecretV2Fn: fake.NewGetSecretV2Fn("nested-secret", nil),
+				PutSecretV2Fn: fake.NewPutSecretV2Fn(errors.New("custom error")),
+			},
+		},
+		"Custom GetSecretV2 Error": {
+			errshould: fmt.Sprintf("failed to push secret at path %q: failed to parse the following okms error: custom error", mySecretRemoteKey),
+			secret:    secretData,
+			data: testingfake.PushSecretData{
+				RemoteKey: mySecretRemoteKey,
+			},
+			okmsClient: fake.FakeOkmsClient{
+				GetSecretV2Fn: fake.NewGetSecretV2Fn("", errors.New("custom error")),
+			},
+		},
+	}
+
+	ctx := context.Background()
+	for name, testCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			cl := ovhClient{
+				okmsClient: testCase.okmsClient,
+			}
+			err := cl.PushSecret(ctx, testCase.secret, testCase.data)
+			if testCase.errshould != "" {
+				if err == nil {
+					t.Errorf("\nexpected error: %s\nactual error:   <nil>\n\n", testCase.errshould)
+				} else if err.Error() != testCase.errshould {
+					t.Errorf("\nexpected error: %s\nactual error:   %v\n\n", testCase.errshould, err)
+				}
+			} else if err != nil {
+				t.Errorf("\nunexpected error: %v\n\n", err)
+			}
+		})
+	}
+}

+ 41 - 0
providers/v1/ovh/client_secret_exists.go

@@ -0,0 +1,41 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+func (cl *ovhClient) SecretExists(ctx context.Context, remoteRef esv1.PushSecretRemoteRef) (bool, error) {
+	remoteKey := remoteRef.GetRemoteKey()
+
+	// Check if the secret exists using the OVH SDK.
+	_, err := cl.okmsClient.GetSecretV2(ctx, cl.okmsID, remoteKey, nil, nil)
+	if err != nil {
+		err = handleOkmsError(err)
+		if errors.Is(err, esv1.NoSecretErr) {
+			return false, nil
+		}
+		return false, fmt.Errorf("failed to check existence of secret %q: %w", remoteKey, err)
+	}
+
+	return true, nil
+}

+ 86 - 0
providers/v1/ovh/client_secret_exists_test.go

@@ -0,0 +1,86 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"testing"
+
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/external-secrets/external-secrets/providers/v1/ovh/fake"
+	testingfake "github.com/external-secrets/external-secrets/runtime/testing/fake"
+)
+
+func TestSecretExists(t *testing.T) {
+	mysecretRef := testingfake.PushSecretData{
+		RemoteKey: "mysecret",
+	}
+	nonExistentSecretRef := testingfake.PushSecretData{
+		RemoteKey: "non-existent-secret",
+	}
+
+	testCases := map[string]struct {
+		should     bool
+		errshould  string
+		remoteRef  testingfake.PushSecretData
+		okmsClient fake.FakeOkmsClient
+		kube       kclient.Client
+	}{
+		"Valid Secret": {
+			should:    true,
+			remoteRef: mysecretRef,
+		},
+		"Non-existent Secret": {
+			should:    false,
+			remoteRef: nonExistentSecretRef,
+		},
+		"Error case": {
+			errshould: fmt.Sprintf("failed to check existence of secret %q: failed to parse the following okms error: custom error", mysecretRef.RemoteKey),
+			remoteRef: mysecretRef,
+			okmsClient: fake.FakeOkmsClient{
+				GetSecretV2Fn: fake.NewGetSecretV2Fn(mysecretRef.RemoteKey, errors.New("custom error")),
+			},
+		},
+	}
+	for name, testCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			cl := &ovhClient{
+				kube:       testCase.kube,
+				okmsClient: testCase.okmsClient,
+			}
+			ctx := context.Background()
+			exists, err := cl.SecretExists(ctx, testCase.remoteRef)
+			if testCase.errshould != "" {
+				if err == nil {
+					t.Errorf("\nexpected error: %s\nactual error:   <nil>\n\n", testCase.errshould)
+				} else if err.Error() != testCase.errshould {
+					t.Errorf("\nexpected error: %s\nactual error:   %v\n\n", testCase.errshould, err)
+				}
+				return
+			} else if err != nil {
+				t.Errorf("\nunexpected error: %v\n\n", err)
+				return
+			}
+			if exists != testCase.should {
+				t.Errorf("\nexpected value: %t\nactual value:   %t\n\n", testCase.should, exists)
+			}
+		})
+	}
+}

+ 124 - 0
providers/v1/ovh/client_utils.go

@@ -0,0 +1,124 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strconv"
+
+	"github.com/google/uuid"
+	"github.com/ovh/okms-sdk-go"
+	"github.com/tidwall/gjson"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+func (cl *ovhClient) getSecretWithOvhSDK(ctx context.Context, okmsID uuid.UUID, ref esv1.ExternalSecretDataRemoteRef) ([]byte, *uint32, error) {
+	// Check if the remoteRef key is empty.
+	if ref.Key == "" {
+		return []byte{}, nil, errors.New("remote key cannot be empty (spec.data.remoteRef.key)")
+	}
+
+	// Check MetaDataPolicy (not supported).
+	if ref.MetadataPolicy == esv1.ExternalSecretMetadataPolicyFetch {
+		return []byte{}, nil, errors.New("fetch metadata policy not supported")
+	}
+
+	// Decode the secret version.
+	versionAddr, err := decodeSecretVersion(ref.Version)
+	if err != nil {
+		return []byte{}, nil, err
+	}
+
+	// Retrieve the KMS secret.
+	includeData := true
+	secret, err := cl.okmsClient.GetSecretV2(ctx, okmsID, ref.Key, versionAddr, &includeData)
+	if err != nil {
+		return []byte{}, nil, handleOkmsError(err)
+	}
+	if secret == nil {
+		return []byte{}, nil, esv1.NoSecretErr
+	}
+	if secret.Version == nil || secret.Version.Data == nil || len(*secret.Version.Data) == 0 {
+		return []byte{}, nil, errors.New("secret version data is missing")
+	}
+
+	// Retrieve KMS Secret property if needed.
+	var secretData []byte
+
+	if ref.Property == "" {
+		secretData, err = json.Marshal(secret.Version.Data)
+	} else {
+		secretData, err = getPropertyValue(*secret.Version.Data, ref.Property)
+	}
+
+	var currentVersion *uint32
+	if secret.Metadata != nil {
+		currentVersion = secret.Metadata.CurrentVersion
+	}
+	return secretData, currentVersion, err
+}
+
+// Decode a secret version.
+//
+// Returns nil if no version is provided; in that case, the OVH SDK uses the latest version.
+func decodeSecretVersion(strVersion string) (*uint32, error) {
+	if strVersion == "" {
+		return nil, nil
+	}
+
+	v, err := strconv.ParseUint(strVersion, 10, 32)
+	if err != nil {
+		return nil, fmt.Errorf("invalid secret version %q: %w", strVersion, err)
+	}
+
+	version := uint32(v)
+	return &version, nil
+}
+
+// Retrieve the value of the secret property.
+func getPropertyValue(data map[string]any, property string) ([]byte, error) {
+	// Marshal data into bytes so it can be passed to gjson.Get.
+	secretData, err := json.Marshal(data)
+	if err != nil {
+		return []byte{}, err
+	}
+
+	// Retrieve the property value if it exists.
+	secretDataResult := gjson.Get(string(secretData), property)
+	if !secretDataResult.Exists() {
+		return []byte{}, fmt.Errorf("secret property %q not found", property)
+	}
+
+	return []byte(secretDataResult.String()), nil
+}
+
+// Returns an okms.KmsError struct representing the KMS response
+// (error_code, error_id, errors, request_id).
+func handleOkmsError(err error) error {
+	okmsError := okms.AsKmsError(err)
+
+	if okmsError == nil {
+		return fmt.Errorf("failed to parse the following okms error: %w", err)
+	} else if okmsError.ErrorCode == 17125377 { // 17125377: returned by OKMS when secret was not found
+		return esv1.NoSecretErr
+	}
+	return okmsError
+}

+ 223 - 0
providers/v1/ovh/fake/fake_okms_client.go

@@ -0,0 +1,223 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package fake
+
+import (
+	"context"
+	"maps"
+
+	"github.com/google/uuid"
+	"github.com/ovh/okms-sdk-go"
+	"github.com/ovh/okms-sdk-go/types"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+type GetSecretV2Fn func() (*types.GetSecretV2Response, error)
+type ListSecretV2Fn func() (*types.ListSecretV2ResponseWithPagination, error)
+type PostSecretV2Fn func() (*types.PostSecretV2Response, error)
+type PutSecretV2Fn func() (*types.PutSecretV2Response, error)
+type DeleteSecretV2Fn func() error
+type WithCustomHeaderFn func() *okms.Client
+type GetSecretsMetadataFn func(path string) (*types.GetMetadataResponse, error)
+
+type FakeOkmsClient struct {
+	GetSecretV2Fn        GetSecretV2Fn
+	ListSecretV2Fn       ListSecretV2Fn
+	PostSecretV2Fn       PostSecretV2Fn
+	PutSecretV2Fn        PutSecretV2Fn
+	DeleteSecretV2Fn     DeleteSecretV2Fn
+	GetSecretsMetadataFn GetSecretsMetadataFn
+}
+
+var (
+	secret = map[string]any{
+		"key1": "value1",
+		"key2": "value2",
+	}
+	nestedSecret = map[string]any{
+		"users": map[string]any{
+			"alice": map[string]string{
+				"age": "23",
+			},
+			"baptist": map[string]string{
+				"age": "27",
+			},
+		},
+	}
+	pattern2TestSecret = map[string]any{
+		"key4": "value4",
+	}
+	pattern2TestDotSecret = map[string]any{
+		"key5": "value5",
+	}
+	pattern2Secret = map[string]any{
+		"key6": "value6",
+	}
+	invalidSecret = map[string]any{
+		"key": "value",
+	}
+	nilSecret   map[string]any = nil
+	emptySecret                = map[string]any{}
+)
+
+var fakeSecretStorage = map[string]map[string]any{
+	"mysecret":                  secret,
+	"nested-secret":             nestedSecret,
+	"pattern2/test/test-secret": pattern2TestSecret,
+	"pattern2/test/test.secret": pattern2TestDotSecret,
+	"pattern2/secret":           pattern2Secret,
+	"invalidpath1//secret":      invalidSecret,
+	"/invalidpath2/secret":      invalidSecret,
+	"invalidpath3/secret//":     invalidSecret,
+	"invalidpath4/secret/":      invalidSecret,
+	"nil/nil-secret":            nilSecret,
+	"nil-secret":                nilSecret,
+	"empty/empty-secret":        emptySecret,
+	"empty-secret":              emptySecret,
+}
+
+var fakeSecretStoragePaths = map[string][]string{
+	"/": {
+		"mysecret",
+		"nested-secret",
+		"pattern2/",
+	},
+	"pattern2": {
+		"test/",
+		"secret",
+	},
+	"pattern2/test": {
+		"test-secret",
+		"test.secret",
+	},
+	"invalidpath1":  {"/secret"},
+	"/invalidpath2": {"secret"},
+	"invalidpath3":  {"secret/"},
+	"invalidpath4":  {"another-secret/"},
+	"nil":           {"nil-secret"},
+	"empty":         {"empty-secret"},
+}
+
+func (f FakeOkmsClient) GetSecretV2(ctx context.Context, okmsID uuid.UUID, path string, version *uint32, includeData *bool) (*types.GetSecretV2Response, error) {
+	if f.GetSecretV2Fn != nil {
+		return f.GetSecretV2Fn()
+	}
+	return NewGetSecretV2Fn(path, nil)()
+}
+
+func NewGetSecretV2Fn(path string, err error) GetSecretV2Fn {
+	if err != nil {
+		return func() (*types.GetSecretV2Response, error) {
+			return nil, err
+		}
+	}
+	return func() (*types.GetSecretV2Response, error) {
+		secret, ok := fakeSecretStorage[path]
+		if !ok {
+			return nil, esv1.NoSecretErr
+		}
+		data := maps.Clone(secret)
+
+		return &types.GetSecretV2Response{
+			Version: &types.SecretV2Version{
+				Data: &data,
+			},
+		}, nil
+	}
+}
+
+func (f FakeOkmsClient) ListSecretV2(ctx context.Context, okmsID uuid.UUID, pageSize *uint32, pageCursor *string) (*types.ListSecretV2ResponseWithPagination, error) {
+	if f.ListSecretV2Fn != nil {
+		return f.ListSecretV2Fn()
+	}
+	return NewListSecretV2Fn(nil)()
+}
+func NewListSecretV2Fn(err error) ListSecretV2Fn {
+	return func() (*types.ListSecretV2ResponseWithPagination, error) {
+		return nil, err
+	}
+}
+
+func (f FakeOkmsClient) PostSecretV2(ctx context.Context, okmsID uuid.UUID, body types.PostSecretV2Request) (*types.PostSecretV2Response, error) {
+	if f.PostSecretV2Fn != nil {
+		return f.PostSecretV2Fn()
+	}
+	return NewPostSecretV2Fn(nil)()
+}
+func NewPostSecretV2Fn(err error) PostSecretV2Fn {
+	return func() (*types.PostSecretV2Response, error) {
+		return nil, err
+	}
+}
+
+func (f FakeOkmsClient) PutSecretV2(ctx context.Context, okmsID uuid.UUID, path string, cas *uint32, body types.PutSecretV2Request) (*types.PutSecretV2Response, error) {
+	if f.PutSecretV2Fn != nil {
+		return f.PutSecretV2Fn()
+	}
+	return NewPutSecretV2Fn(nil)()
+}
+func NewPutSecretV2Fn(err error) PutSecretV2Fn {
+	return func() (*types.PutSecretV2Response, error) {
+		return nil, err
+	}
+}
+
+func (f FakeOkmsClient) DeleteSecretV2(ctx context.Context, okmsID uuid.UUID, path string) error {
+	if f.DeleteSecretV2Fn != nil {
+		return f.DeleteSecretV2Fn()
+	}
+	return NewDeleteSecretV2Fn(nil)()
+}
+func NewDeleteSecretV2Fn(err error) DeleteSecretV2Fn {
+	return func() error {
+		return err
+	}
+}
+
+// GetSecretsMetadata is a mock implementation of the OVH SDK GetSecretsMetadata method.
+// It returns metadata for all secrets under the given path.
+//
+// Keys ending with a '/' indicate subpaths, meaning the key represents a folder rather
+// than a final secret value.
+//
+// This implementation returns a list of secrets from fakeSecretStorage variable.
+func (f FakeOkmsClient) GetSecretsMetadata(ctx context.Context, okmsID uuid.UUID, path string, list bool) (*types.GetMetadataResponse, error) {
+	if f.GetSecretsMetadataFn != nil {
+		return f.GetSecretsMetadataFn(path)
+	}
+
+	if path == "" {
+		path = "/"
+	}
+	keys, ok := fakeSecretStoragePaths[path]
+	if !ok {
+		return nil, nil
+	}
+
+	resp := &types.GetMetadataResponse{
+		Data: &types.SecretMetadata{
+			Keys: &keys,
+		},
+	}
+
+	return resp, nil
+}
+
+func (f FakeOkmsClient) WithCustomHeader(key, value string) *okms.Client {
+	return nil
+}

+ 85 - 0
providers/v1/ovh/fake/fake_resolver.go

@@ -0,0 +1,85 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package fake
+
+import (
+	"context"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"encoding/pem"
+
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+type FakeSecretKeyResolver struct {
+	privateKey *rsa.PrivateKey
+}
+
+func (fr *FakeSecretKeyResolver) Resolve(_ context.Context, _ kclient.Client, _, _ string, ref esmeta.SecretKeySelector) (string, error) {
+	switch ref.Name {
+	case "Valid token auth":
+		return "Valid", nil
+	case "Valid mtls client certificate":
+		return fr.generateFakeCert()
+	case "Valid mtls client key":
+		return fr.generateFakeKey()
+	default:
+		return "", nil
+	}
+}
+
+func (fr *FakeSecretKeyResolver) generateFakeKey() (string, error) {
+	var err error
+
+	fr.privateKey, err = rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		return "", err
+	}
+
+	err = fr.privateKey.Validate()
+	if err != nil {
+		return "", err
+	}
+
+	privateKeyDER := x509.MarshalPKCS1PrivateKey(fr.privateKey)
+	pemBlock := pem.Block{
+		Type:  "RSA PRIVATE KEY",
+		Bytes: privateKeyDER,
+	}
+	privateKeyPEM := pem.EncodeToMemory(&pemBlock)
+
+	return string(privateKeyPEM), nil
+}
+
+func (fr *FakeSecretKeyResolver) generateFakeCert() (string, error) {
+	template := x509.Certificate{}
+	cert, err := x509.CreateCertificate(rand.Reader, &template, &template, &fr.privateKey.PublicKey, fr.privateKey)
+	if err != nil {
+		return "", err
+	}
+
+	pemBlock := pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: cert,
+	}
+	certPEM := pem.EncodeToMemory(&pemBlock)
+
+	return string(certPEM), nil
+}

+ 68 - 0
providers/v1/ovh/fake/fake_resolver_test.go

@@ -0,0 +1,68 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package fake
+
+import (
+	"context"
+	"encoding/pem"
+	"errors"
+	"fmt"
+	"testing"
+
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+func TestResolve(t *testing.T) {
+	fr := &FakeSecretKeyResolver{}
+
+	// TESTING FAKE PRIVATE KEY GENERATION
+	t.Run("Fake Private Key Generation", func(t *testing.T) {
+		privKeyPEM, err := fr.Resolve(context.Background(), nil, "", "", esmeta.SecretKeySelector{Name: "Valid mtls client key"})
+		if err != nil {
+			t.Errorf("Failed to generate fake private key: %v", err)
+		}
+
+		if err := decodePEM(privKeyPEM, "RSA PRIVATE KEY"); err != nil {
+			t.Error(err)
+		}
+	})
+
+	// TESTING FAKE CLIENT CERTIFICATE GENERATION
+	t.Run("Fake Client Certificate Generation", func(t *testing.T) {
+		certPEM, err := fr.Resolve(context.Background(), nil, "", "", esmeta.SecretKeySelector{Name: "Valid mtls client certificate"})
+		if err != nil {
+			t.Errorf("Failed to generate fake client certificate: %v", err)
+		}
+
+		if err := decodePEM(certPEM, "CERTIFICATE"); err != nil {
+			t.Error(err)
+		}
+	})
+}
+
+func decodePEM(pemStr, expectedType string) error {
+	pemBlock, _ := pem.Decode([]byte(pemStr))
+	if pemBlock == nil {
+		return errors.New("Failed to decode PEM (client certificate): got nil")
+	}
+
+	if pemBlock.Type != expectedType {
+		return fmt.Errorf("Unexpected PEM type: got %s, want %s", pemBlock.Type, expectedType)
+	}
+
+	return nil
+}

+ 113 - 0
providers/v1/ovh/go.mod

@@ -0,0 +1,113 @@
+module github.com/external-secrets/external-secrets/providers/v1/ovh
+
+go 1.26.1
+
+require (
+	github.com/external-secrets/external-secrets/apis v0.0.0
+	github.com/external-secrets/external-secrets/runtime v0.0.0
+	github.com/google/uuid v1.6.0
+	github.com/ovh/okms-sdk-go v0.5.1
+	github.com/tidwall/gjson v1.18.0
+	k8s.io/api v0.35.2
+	k8s.io/apimachinery v0.35.2
+	sigs.k8s.io/controller-runtime v0.23.3
+)
+
+require (
+	dario.cat/mergo v1.0.2 // indirect
+	github.com/Masterminds/goutils v1.1.1 // indirect
+	github.com/Masterminds/semver/v3 v3.4.0 // indirect
+	github.com/Masterminds/sprig/v3 v3.3.1-0.20241028115027-8cb06fe3c8b0 // indirect
+	github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
+	github.com/aws/aws-sdk-go-v2 v1.39.6 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
+	github.com/emicklei/go-restful/v3 v3.13.0 // indirect
+	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
+	github.com/fatih/color v1.18.0 // indirect
+	github.com/fsnotify/fsnotify v1.9.0 // indirect
+	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
+	github.com/go-openapi/jsonpointer v0.22.5 // indirect
+	github.com/go-openapi/jsonreference v0.21.5 // indirect
+	github.com/go-openapi/swag v0.25.5 // indirect
+	github.com/go-openapi/swag/cmdutils v0.25.5 // indirect
+	github.com/go-openapi/swag/conv v0.25.5 // indirect
+	github.com/go-openapi/swag/fileutils v0.25.5 // indirect
+	github.com/go-openapi/swag/jsonname v0.25.5 // indirect
+	github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
+	github.com/go-openapi/swag/loading v0.25.5 // indirect
+	github.com/go-openapi/swag/mangling v0.25.5 // indirect
+	github.com/go-openapi/swag/netutils v0.25.5 // indirect
+	github.com/go-openapi/swag/stringutils v0.25.5 // indirect
+	github.com/go-openapi/swag/typeutils v0.25.5 // indirect
+	github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
+	github.com/goccy/go-json v0.10.5 // indirect
+	github.com/gofrs/flock v0.13.0 // indirect
+	github.com/google/btree v1.1.3 // indirect
+	github.com/google/gnostic-models v0.7.1 // indirect
+	github.com/google/go-cmp v0.7.0 // indirect
+	github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
+	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+	github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
+	github.com/huandu/xstrings v1.5.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/lestrrat-go/blackmagic v1.0.4 // indirect
+	github.com/lestrrat-go/httpcc v1.0.1 // indirect
+	github.com/lestrrat-go/httprc v1.0.6 // indirect
+	github.com/lestrrat-go/iter v1.0.2 // indirect
+	github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect
+	github.com/lestrrat-go/option v1.0.1 // indirect
+	github.com/mattn/go-colorable v0.1.14 // indirect
+	github.com/mitchellh/copystructure v1.2.0 // indirect
+	github.com/mitchellh/reflectwalk v1.0.2 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/oapi-codegen/runtime v1.1.2 // indirect
+	github.com/oracle/oci-go-sdk/v65 v65.103.0 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/prometheus/client_golang v1.23.2 // indirect
+	github.com/prometheus/client_model v0.6.2 // indirect
+	github.com/prometheus/common v0.67.5 // indirect
+	github.com/prometheus/procfs v0.20.1 // indirect
+	github.com/segmentio/asm v1.2.1 // indirect
+	github.com/shopspring/decimal v1.4.0 // indirect
+	github.com/sony/gobreaker v1.0.0 // indirect
+	github.com/spf13/cast v1.10.0 // indirect
+	github.com/spf13/pflag v1.0.10 // indirect
+	github.com/tidwall/match v1.2.0 // indirect
+	github.com/tidwall/pretty v1.2.1 // indirect
+	github.com/x448/float16 v0.8.4 // indirect
+	go.yaml.in/yaml/v2 v2.4.4 // indirect
+	go.yaml.in/yaml/v3 v3.0.4 // indirect
+	golang.org/x/crypto v0.48.0 // indirect
+	golang.org/x/net v0.51.0 // indirect
+	golang.org/x/oauth2 v0.36.0 // indirect
+	golang.org/x/sync v0.20.0 // indirect
+	golang.org/x/sys v0.42.0 // indirect
+	golang.org/x/term v0.40.0 // indirect
+	golang.org/x/text v0.34.0 // indirect
+	golang.org/x/time v0.15.0 // indirect
+	gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
+	google.golang.org/protobuf v1.36.11 // indirect
+	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
+	gopkg.in/inf.v0 v0.9.1 // indirect
+	k8s.io/apiextensions-apiserver v0.35.2 // indirect
+	k8s.io/client-go v0.35.2 // indirect
+	k8s.io/klog/v2 v2.140.0 // indirect
+	k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf // indirect
+	k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
+	sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
+	sigs.k8s.io/randfill v1.0.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
+	sigs.k8s.io/yaml v1.6.0 // indirect
+	software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect
+)
+
+replace (
+	github.com/external-secrets/external-secrets/apis => ../../../apis
+	github.com/external-secrets/external-secrets/runtime => ../../../runtime
+)

+ 270 - 0
providers/v1/ovh/go.sum

@@ -0,0 +1,270 @@
+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
+github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/Masterminds/sprig/v3 v3.3.1-0.20241028115027-8cb06fe3c8b0 h1:ecMw5jYFlWLY7EP3IKOELA8/CTy6cT/biq36sPZpBtw=
+github.com/Masterminds/sprig/v3 v3.3.1-0.20241028115027-8cb06fe3c8b0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
+github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
+github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
+github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
+github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
+github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
+github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
+github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
+github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
+github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
+github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
+github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
+github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
+github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
+github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
+github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
+github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
+github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
+github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU=
+github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA=
+github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c=
+github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
+github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
+github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
+github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk=
+github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc=
+github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
+github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
+github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
+github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
+github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
+github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
+github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw=
+github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY=
+github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU=
+github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14=
+github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
+github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
+github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
+github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
+github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
+github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
+github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag=
+github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=
+github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
+github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
+github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
+github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
+github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
+github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
+github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
+github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
+github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
+github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
+github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
+github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
+github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
+github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
+github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
+github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
+github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
+github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
+github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA=
+github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU=
+github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
+github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI=
+github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
+github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
+github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
+github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
+github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
+github.com/oracle/oci-go-sdk/v65 v65.103.0 h1:HfyZx+JefCPK3At0Xt45q+wr914jDXuoyzOFX3XCbno=
+github.com/oracle/oci-go-sdk/v65 v65.103.0/go.mod h1:oB8jFGVc/7/zJ+DbleE8MzGHjhs2ioCz5stRTdZdIcY=
+github.com/ovh/okms-sdk-go v0.5.1 h1:oS8w/BXyGgnBzaGh3zFIyw73LwJ2B+UkNqeVBT2epUU=
+github.com/ovh/okms-sdk-go v0.5.1/go.mod h1:dJpK0dmnRphZgttfsjg6c5yLgcZp6LXPR/6CFwxY124=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
+github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
+github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
+github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
+github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
+github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
+github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
+github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
+github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
+github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
+github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
+github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
+go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
+golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
+golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
+golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
+golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
+golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
+golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
+golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
+golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
+golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
+golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
+golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
+golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
+golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
+golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
+gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
+gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
+gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
+k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
+k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0=
+k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU=
+k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
+k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
+k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=
+k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=
+k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
+k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
+k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf h1:btPscg4cMql0XdYK2jLsJcNEKmACJz8l+U7geC06FiM=
+k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
+k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
+k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
+sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80=
+sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0=
+software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

+ 355 - 0
providers/v1/ovh/provider.go

@@ -0,0 +1,355 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh implements a provider that enables synchronization with OVHcloud's Secret Manager.
+package ovh
+
+import (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+	"reflect"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/ovh/okms-sdk-go"
+	"github.com/ovh/okms-sdk-go/types"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/runtime/esutils"
+	"github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
+)
+
+const (
+	emptyTokenSecretRef           = "ovh store auth.token.tokenSecretRef cannot be empty"
+	emptyKeySecretRef             = "ovh store auth.mtls.keySecretRef cannot be empty"
+	emptyCertSecretRef            = "ovh store auth.mtls.certSecretRef cannot be empty"
+	createOvhProviderError        = "failed to create new ovh provider client"
+	createOkmsClientError         = "failed to create new okms client"
+	configureTokenOkmsClientError = "failed to configure token okms client"
+	configureMtlsOkmsClientError  = "failed to configure mtls okms client"
+)
+
+// Provider implements the ESO Provider interface for OVHcloud.
+type Provider struct {
+	secretKeyResolver SecretKeyResolver
+}
+
+// OkmsClient defines an interface for interacting with the OVH OKMS service.
+// It allows for both real API calls and mocking for unit tests.
+type OkmsClient interface {
+	GetSecretV2(ctx context.Context, okmsID uuid.UUID, path string, version *uint32, includeData *bool) (*types.GetSecretV2Response, error)
+	ListSecretV2(ctx context.Context, okmsID uuid.UUID, pageSize *uint32, pageCursor *string) (*types.ListSecretV2ResponseWithPagination, error)
+	PostSecretV2(ctx context.Context, okmsID uuid.UUID, body types.PostSecretV2Request) (*types.PostSecretV2Response, error)
+	PutSecretV2(ctx context.Context, okmsID uuid.UUID, path string, cas *uint32, body types.PutSecretV2Request) (*types.PutSecretV2Response, error)
+	DeleteSecretV2(ctx context.Context, okmsID uuid.UUID, path string) error
+	WithCustomHeader(key, value string) *okms.Client
+	GetSecretsMetadata(ctx context.Context, okmsID uuid.UUID, path string, list bool) (*types.GetMetadataResponse, error)
+}
+
+// SecretKeyResolver resolves the value of a key from a Kubernetes Secret.
+// It is defined as an interface to allow different implementations, including mocks for testing.
+type SecretKeyResolver interface {
+	Resolve(ctx context.Context, kube kclient.Client, ovhStoreKind string, ovhStoreNameSpace string, secretRef v1.SecretKeySelector) (string, error)
+}
+
+// DefaultSecretKeyResolver is the default implementation for resolving keys from Kubernetes Secrets.
+type DefaultSecretKeyResolver struct{}
+
+type ovhClient struct {
+	ovhStoreNameSpace string
+	ovhStoreKind      string
+	kube              kclient.Client
+	okmsID            uuid.UUID
+	cas               bool
+	okmsTimeout       time.Duration
+	okmsClient        OkmsClient
+}
+
+var _ esv1.SecretsClient = &ovhClient{}
+
+// Resolve returns the value of the referenced key from a Kubernetes Secret.
+func (r DefaultSecretKeyResolver) Resolve(ctx context.Context, kube kclient.Client, ovhStoreKind, ovhStoreNameSpace string, secretRef v1.SecretKeySelector) (string, error) {
+	return resolvers.SecretKeyRef(ctx, kube, ovhStoreKind, ovhStoreNameSpace, &secretRef)
+}
+
+// NewClient creates a new Provider client.
+func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
+	// Validate Store before creating a client from it.
+	_, err := p.ValidateStore(store)
+	if err != nil {
+		return nil, fmt.Errorf("%s: store validation failed: %w", createOvhProviderError, err)
+	}
+
+	if kube == nil {
+		return nil, fmt.Errorf("%s: controller-runtime client is nil", createOvhProviderError)
+	}
+
+	ovhStore := store.GetSpec().Provider.OVHcloud
+	// ovhClient configuration.
+	okmsID, err := uuid.Parse(ovhStore.OkmsID)
+	if err != nil {
+		return nil, fmt.Errorf("%s: could not parse okmsID: %w", createOvhProviderError, err)
+	}
+
+	cas := false
+	if ovhStore.CasRequired != nil {
+		cas = *ovhStore.CasRequired
+	}
+
+	okmsTimeout := 30 * time.Second
+	if ovhStore.OkmsTimeout != nil {
+		okmsTimeout = time.Duration(*ovhStore.OkmsTimeout) * time.Second
+	}
+	cl := &ovhClient{
+		ovhStoreNameSpace: namespace,
+		ovhStoreKind:      store.GetKind(),
+		kube:              kube,
+		okmsID:            okmsID,
+		cas:               cas,
+		okmsTimeout:       okmsTimeout,
+	}
+
+	// Authentication configuration: token or mTLS.
+	if ovhStore.Auth.ClientToken != nil {
+		err = configureHTTPTokenClient(ctx, p, cl,
+			ovhStore.Server, ovhStore.Auth.ClientToken)
+	} else if ovhStore.Auth.ClientMTLS != nil {
+		err = configureHTTPMTLSClient(ctx, p, cl,
+			ovhStore.Server, ovhStore.Auth.ClientMTLS)
+	}
+	if err != nil {
+		return nil, fmt.Errorf("%s: %w", createOvhProviderError, err)
+	}
+	return cl, nil
+}
+
+// configureHTTPTokenClient clientConfigure the client to use the provided token for HTTP requests.
+func configureHTTPTokenClient(ctx context.Context, p *Provider, cl *ovhClient, server string, clientToken *esv1.OvhClientToken) error {
+	token, err := getToken(ctx, p, cl, clientToken)
+	if err != nil {
+		return fmt.Errorf("%s: could not retrieve token: %w", configureTokenOkmsClientError, err)
+	}
+	bearerToken := fmt.Sprintf("Bearer %s", token)
+
+	// Request a new OKMS client from the OVH SDK.
+	httpClient := &http.Client{
+		Timeout: cl.okmsTimeout,
+	}
+	cl.okmsClient, err = okms.NewRestAPIClientWithHttp(server, httpClient)
+	if err != nil {
+		return fmt.Errorf("%s: %s: %w", configureTokenOkmsClientError, createOkmsClientError, err)
+	}
+	if cl.okmsClient == nil {
+		return fmt.Errorf("%s: okms client is nil", configureTokenOkmsClientError)
+	}
+
+	// Add a custom header.
+	cl.okmsClient.WithCustomHeader("Authorization", bearerToken)
+	cl.okmsClient.WithCustomHeader("Content-type", "application/json")
+
+	return nil
+}
+
+// getToken retrieves the token value from the Kubernetes secret.
+func getToken(ctx context.Context, p *Provider, cl *ovhClient, clientToken *esv1.OvhClientToken) (string, error) {
+	// ClienTokenSecret refers to the Kubernetes secret that stores the token.
+	tokenSecretRef := clientToken.ClientTokenSecret
+
+	// Retrieve the token value.
+	token, err := p.secretKeyResolver.Resolve(ctx, cl.kube,
+		cl.ovhStoreKind, cl.ovhStoreNameSpace, tokenSecretRef)
+	if err != nil {
+		return "", fmt.Errorf("failed to resolve token secret ref: %w", err)
+	}
+	if token == "" {
+		return "", errors.New(emptyTokenSecretRef)
+	}
+
+	return token, nil
+}
+
+// configureHTTPMTLSClient configures the client to use mTLS for HTTP requests.
+func configureHTTPMTLSClient(ctx context.Context, p *Provider, cl *ovhClient, server string, clientMTLS *esv1.OvhClientMTLS) error {
+	httpClient, err := newHTTPClientWithMTLS(ctx, p, cl, clientMTLS)
+	if err != nil {
+		return fmt.Errorf("%s: could not create http client:%w", configureMtlsOkmsClientError, err)
+	}
+
+	// Request a new OKMS client from the OVH SDK (mTLS configured).
+	cl.okmsClient, err = okms.NewRestAPIClientWithHttp(server, httpClient)
+	if err != nil {
+		return fmt.Errorf("%s: %s: %w", configureMtlsOkmsClientError, createOkmsClientError, err)
+	}
+	if cl.okmsClient == nil {
+		return fmt.Errorf("%s: okms client is nil", configureMtlsOkmsClientError)
+	}
+
+	return nil
+}
+
+// getClientConfig creates an HTTP client configured for MTLS using the provided
+// client certificate and key, and optionally adds a custom CA from CAProvider or CABundle.
+func newHTTPClientWithMTLS(ctx context.Context, p *Provider, cl *ovhClient, clientMTLS *esv1.OvhClientMTLS) (*http.Client, error) {
+	cert, err := buildX509Certificate(ctx, cl, p, clientMTLS)
+	if err != nil {
+		return nil, fmt.Errorf("failed to build x509 certificate: %w", err)
+	}
+
+	// Create an HTTP transport for mTLS, enforcing TLS 1.2+ and using the client certificate.
+	transport := http.DefaultTransport.(*http.Transport).Clone()
+	transport.TLSClientConfig = &tls.Config{
+		MinVersion:   tls.VersionTLS12,
+		Certificates: []tls.Certificate{cert},
+	}
+	// Configure custom CA for the TLS client if provided via CAProvider or CABundle.
+	if clientMTLS.CAProvider != nil || len(clientMTLS.CABundle) != 0 {
+		caCertPool := x509.NewCertPool()
+		ca, err := esutils.FetchCACertFromSource(ctx, esutils.CreateCertOpts{
+			CABundle:   clientMTLS.CABundle,
+			CAProvider: clientMTLS.CAProvider,
+			StoreKind:  cl.ovhStoreKind,
+			Namespace:  cl.ovhStoreNameSpace,
+			Client:     cl.kube,
+		})
+		if err != nil {
+			return nil, fmt.Errorf("failed to fetch CA cert: %w", err)
+		}
+		if !caCertPool.AppendCertsFromPEM(ca) {
+			return nil, fmt.Errorf("failed to append CA")
+		}
+		transport.TLSClientConfig.RootCAs = caCertPool
+	}
+	// Build the HTTP client with configured transport and timeout.
+	httpClient := http.Client{
+		Timeout:   cl.okmsTimeout,
+		Transport: transport,
+	}
+	return &httpClient, nil
+}
+
+// buildX509Certificate retrieves client certificate and key to build X509 Certificate.
+func buildX509Certificate(ctx context.Context, cl *ovhClient, p *Provider, clientMTLS *esv1.OvhClientMTLS) (tls.Certificate, error) {
+	clientKey, err := resolveSecretValue(ctx, cl, p, clientMTLS.ClientKey, emptyKeySecretRef)
+	if err != nil {
+		return tls.Certificate{}, fmt.Errorf("failed to resolve client key: %w", err)
+	}
+	clientCert, err := resolveSecretValue(ctx, cl, p, clientMTLS.ClientCertificate, emptyCertSecretRef)
+	if err != nil {
+		return tls.Certificate{}, fmt.Errorf("failed to resolve client certificate: %w", err)
+	}
+	cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
+	if err != nil {
+		return tls.Certificate{}, fmt.Errorf("failed to create x509 key pair: %w", err)
+	}
+	return cert, nil
+}
+
+// resolveSecret retrieves the value of the client certificate and key.
+func resolveSecretValue(ctx context.Context, cl *ovhClient, p *Provider, ref v1.SecretKeySelector, errMsg string) (string, error) {
+	// ref refers to the Kubernetes secret object.
+	// Retrieve the value of ref.
+	secret, err := p.secretKeyResolver.Resolve(ctx, cl.kube,
+		cl.ovhStoreKind, cl.ovhStoreNameSpace, ref)
+	if err != nil {
+		return "", fmt.Errorf("failed to resolve secret value: %w", err)
+	}
+	if secret == "" {
+		return "", errors.New(errMsg)
+	}
+	return secret, nil
+}
+
+// ValidateStore statically validate the Secret Store specification.
+func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
+	// Nil checks.
+	if store == nil || reflect.ValueOf(store).IsNil() {
+		return nil, errors.New("store is nil")
+	}
+	spec := store.GetSpec()
+	if spec == nil {
+		return nil, errors.New("store spec is nil")
+	}
+	provider := spec.Provider
+	if provider == nil {
+		return nil, errors.New("store provider is nil")
+	}
+	if provider.OVHcloud == nil {
+		return nil, errors.New("ovh store provider is nil")
+	}
+	if provider.OVHcloud.OkmsTimeout != nil && *provider.OVHcloud.OkmsTimeout <= 0 {
+		return nil, errors.New("ovh store okmsTimeout must be greater than 0")
+	}
+	if provider.OVHcloud.Server == "" {
+		return nil, errors.New("ovh store server is required")
+	}
+	if _, err := url.Parse(provider.OVHcloud.Server); err != nil {
+		return nil, fmt.Errorf("ovh store server must contain a valid url: %w", err)
+	}
+	if provider.OVHcloud.OkmsID == "" {
+		return nil, errors.New("ovh store okmsID is required")
+	}
+
+	// Validate the provider's authentication method.
+	return p.validateAuth(provider)
+}
+
+func (p *Provider) validateAuth(provider *esv1.SecretStoreProvider) (admission.Warnings, error) {
+	auth := provider.OVHcloud.Auth
+	if auth.ClientMTLS == nil && auth.ClientToken == nil {
+		return nil, errors.New("missing authentication method")
+	} else if auth.ClientMTLS != nil && auth.ClientToken != nil {
+		return nil, errors.New("only one authentication method allowed (mtls | token)")
+	}
+	if auth.ClientToken != nil && auth.ClientToken.ClientTokenSecret == (v1.SecretKeySelector{}) {
+		return nil, errors.New("missing token secret for token authentication")
+	}
+	if auth.ClientMTLS != nil && (auth.ClientMTLS.ClientCertificate == (v1.SecretKeySelector{}) || auth.ClientMTLS.ClientKey == (v1.SecretKeySelector{})) {
+		return nil, errors.New("missing tls certificate or key for mtls authentication")
+	}
+	return nil, nil
+}
+
+// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
+func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
+	return esv1.SecretStoreReadWrite
+}
+
+// NewProvider creates a new Provider instance.
+func NewProvider() esv1.Provider {
+	return &Provider{
+		secretKeyResolver: DefaultSecretKeyResolver{},
+	}
+}
+
+// ProviderSpec returns the provider specification for registration.
+func ProviderSpec() *esv1.SecretStoreProvider {
+	return &esv1.SecretStoreProvider{
+		OVHcloud: &esv1.OvhProvider{},
+	}
+}
+
+// MaintenanceStatus returns the maintenance status of the provider.
+func MaintenanceStatus() esv1.MaintenanceStatus {
+	return esv1.MaintenanceStatusMaintained
+}

+ 455 - 0
providers/v1/ovh/provider_test.go

@@ -0,0 +1,455 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+	fakeBuilder "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/providers/v1/ovh/fake"
+)
+
+var (
+	namespace = "namespace"
+	scheme    = runtime.NewScheme()
+	_         = corev1.AddToScheme(scheme)
+	kube      = fakeBuilder.NewClientBuilder().
+			WithScheme(scheme).
+			WithObjects(&corev1.Secret{
+			ObjectMeta: v1.ObjectMeta{
+				Name:      "my-secret",
+				Namespace: "default",
+			},
+			Data: map[string][]byte{
+				"key": []byte("value"),
+			},
+		}).Build()
+	okmsId          = "11111111-1111-1111-1111-111111111111"
+	validTokenAuth  = "Valid token auth"
+	validClientCert = "Valid mtls client certificate"
+	validClientKey  = "Valid mtls client key"
+	fillingStr      = "string"
+)
+
+func TestNewClient(t *testing.T) {
+	tests := map[string]struct {
+		errshould string
+		kube      kclient.Client
+		store     *esv1.SecretStore
+	}{
+		"Nil store": {
+			errshould: "failed to create new ovh provider client: store validation failed: store is nil",
+			kube:      kube,
+		},
+		"Nil provider": {
+			errshould: "failed to create new ovh provider client: store validation failed: store provider is nil",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: nil,
+				},
+			},
+		},
+		"Nil ovh provider": {
+			errshould: "failed to create new ovh provider client: store validation failed: ovh store provider is nil",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{},
+					},
+				},
+			},
+		},
+		"Nil controller-runtime client": {
+			errshould: "failed to create new ovh provider client: controller-runtime client is nil",
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						OVHcloud: &esv1.OvhProvider{
+							Auth: esv1.OvhAuth{
+								ClientToken: &esv1.OvhClientToken{
+									ClientTokenSecret: esmeta.SecretKeySelector{
+										Name:      validTokenAuth,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+								},
+							},
+							Server: fillingStr,
+							OkmsID: okmsId,
+						},
+					},
+				},
+			},
+		},
+		"Authentication method conflict": {
+			errshould: "failed to create new ovh provider client: store validation failed: only one authentication method allowed (mtls | token)",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						OVHcloud: &esv1.OvhProvider{
+							Server: fillingStr,
+							OkmsID: okmsId,
+							Auth: esv1.OvhAuth{
+								ClientMTLS: &esv1.OvhClientMTLS{
+									ClientCertificate: esmeta.SecretKeySelector{
+										Name:      fillingStr,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+									ClientKey: esmeta.SecretKeySelector{
+										Name:      fillingStr,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+								},
+								ClientToken: &esv1.OvhClientToken{
+									ClientTokenSecret: esmeta.SecretKeySelector{
+										Name:      fillingStr,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		"Authentication method empty": {
+			errshould: "failed to create new ovh provider client: store validation failed: missing authentication method",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						OVHcloud: &esv1.OvhProvider{
+							Server: fillingStr,
+							OkmsID: okmsId,
+							Auth:   esv1.OvhAuth{},
+						},
+					},
+				},
+			},
+		},
+		"Valid token auth": {
+			errshould: "",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						OVHcloud: &esv1.OvhProvider{
+							Server: fillingStr,
+							OkmsID: okmsId,
+							Auth: esv1.OvhAuth{
+								ClientToken: &esv1.OvhClientToken{
+									ClientTokenSecret: esmeta.SecretKeySelector{
+										Name:      validTokenAuth,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		"Empty token auth": {
+			errshould: "failed to create new ovh provider client: store validation failed: missing token secret for token authentication",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						OVHcloud: &esv1.OvhProvider{
+							Server: fillingStr,
+							OkmsID: okmsId,
+							Auth: esv1.OvhAuth{
+								ClientToken: &esv1.OvhClientToken{
+									ClientTokenSecret: esmeta.SecretKeySelector{},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		"Valid mtls auth": {
+			errshould: "",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						OVHcloud: &esv1.OvhProvider{
+							Server: fillingStr,
+							OkmsID: okmsId,
+							Auth: esv1.OvhAuth{
+								ClientMTLS: &esv1.OvhClientMTLS{
+									ClientCertificate: esmeta.SecretKeySelector{
+										Name:      validClientCert,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+									ClientKey: esmeta.SecretKeySelector{
+										Name:      validClientKey,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		"Empty mtls client certificate": {
+			errshould: "failed to create new ovh provider client: store validation failed: missing tls certificate or key for mtls authentication",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						OVHcloud: &esv1.OvhProvider{
+							Server: fillingStr,
+							OkmsID: okmsId,
+							Auth: esv1.OvhAuth{
+								ClientMTLS: &esv1.OvhClientMTLS{
+									ClientKey: esmeta.SecretKeySelector{
+										Name:      validClientKey,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	ctx := context.Background()
+	for name, testCase := range tests {
+		t.Run(name, func(t *testing.T) {
+			provider := Provider{
+				secretKeyResolver: &fake.FakeSecretKeyResolver{},
+			}
+			_, err := provider.NewClient(ctx, testCase.store, testCase.kube, "namespace")
+			if testCase.errshould != "" {
+				if err == nil {
+					t.Errorf("\nexpected error: %s\nactual error:   <nil>\n\n", testCase.errshould)
+				} else if err.Error() != testCase.errshould {
+					t.Errorf("\nexpected error: %s\nactual error:   %v\n\n", testCase.errshould, err)
+				}
+			} else if err != nil {
+				t.Errorf("\nunexpected error: %v\n\n", err)
+			}
+		})
+	}
+}
+
+func TestValidateStore(t *testing.T) {
+	var namespace string = "namespace"
+	tests := map[string]struct {
+		errshould string
+		kube      kclient.Client
+		store     *esv1.SecretStore
+	}{
+		"Nil store": {
+			errshould: "store provider is nil",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: nil,
+				},
+			},
+		},
+		"Nil ovh provider": {
+			errshould: "ovh store provider is nil",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						AWS: &esv1.AWSProvider{},
+					},
+				},
+			},
+		},
+		"Authentication method conflict": {
+			errshould: "only one authentication method allowed (mtls | token)",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						OVHcloud: &esv1.OvhProvider{
+							Server: fillingStr,
+							OkmsID: okmsId,
+							Auth: esv1.OvhAuth{
+								ClientMTLS: &esv1.OvhClientMTLS{
+									ClientCertificate: esmeta.SecretKeySelector{
+										Name:      fillingStr,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+									ClientKey: esmeta.SecretKeySelector{
+										Name:      fillingStr,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+								},
+								ClientToken: &esv1.OvhClientToken{
+									ClientTokenSecret: esmeta.SecretKeySelector{
+										Name:      fillingStr,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		"Valid token auth": {
+			errshould: "",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						OVHcloud: &esv1.OvhProvider{
+							Server: fillingStr,
+							OkmsID: okmsId,
+							Auth: esv1.OvhAuth{
+								ClientToken: &esv1.OvhClientToken{
+									ClientTokenSecret: esmeta.SecretKeySelector{
+										Name:      fillingStr,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		"Valid mtls auth": {
+			errshould: "",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						OVHcloud: &esv1.OvhProvider{
+							Server: fillingStr,
+							OkmsID: okmsId,
+							Auth: esv1.OvhAuth{
+								ClientMTLS: &esv1.OvhClientMTLS{
+									ClientCertificate: esmeta.SecretKeySelector{
+										Name:      fillingStr,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+									ClientKey: esmeta.SecretKeySelector{
+										Name:      fillingStr,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		"Invalid mtls auth: missing client certificate": {
+			errshould: "missing tls certificate or key for mtls authentication",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						OVHcloud: &esv1.OvhProvider{
+							Server: fillingStr,
+							OkmsID: okmsId,
+							Auth: esv1.OvhAuth{
+								ClientMTLS: &esv1.OvhClientMTLS{
+									ClientKey: esmeta.SecretKeySelector{
+										Name:      fillingStr,
+										Namespace: &namespace,
+										Key:       fillingStr,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		"Empty auth": {
+			errshould: "missing authentication method",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						OVHcloud: &esv1.OvhProvider{
+							Server: fillingStr,
+							OkmsID: okmsId,
+						},
+					},
+				},
+			},
+		},
+		"Invalid token auth: missing token secret": {
+			errshould: "missing token secret for token authentication",
+			kube:      kube,
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						OVHcloud: &esv1.OvhProvider{
+							Server: fillingStr,
+							OkmsID: okmsId,
+							Auth: esv1.OvhAuth{
+								ClientToken: &esv1.OvhClientToken{},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	for name, testCase := range tests {
+		t.Run(name, func(t *testing.T) {
+			provider := Provider{}
+			_, err := provider.ValidateStore(testCase.store)
+			if testCase.errshould != "" {
+				if err == nil {
+					t.Errorf("\nexpected error: %s\nactual error:   <nil>\n\n", testCase.errshould)
+				} else if err.Error() != testCase.errshould {
+					t.Errorf("\nexpected error: %s\nactual error:   %v\n\n", testCase.errshould, err)
+				}
+			} else if err != nil {
+				t.Errorf("\nunexpected error: %v\n\n", err)
+			}
+		})
+	}
+}

+ 35 - 0
providers/v1/ovh/validate.go

@@ -0,0 +1,35 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"context"
+	"fmt"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+// Dynamically validate the Secret Store configuration.
+//
+// An HTTP request is sent to the provider to verify authorization.
+func (cl *ovhClient) Validate() (esv1.ValidationResult, error) {
+	_, err := cl.okmsClient.ListSecretV2(context.Background(), cl.okmsID, nil, nil)
+	if err != nil {
+		return esv1.ValidationResultError, fmt.Errorf("failed to validate secret store: %w", err)
+	}
+	return esv1.ValidationResultReady, nil
+}

+ 64 - 0
providers/v1/ovh/validate_test.go

@@ -0,0 +1,64 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 ovh
+
+import (
+	"errors"
+	"testing"
+
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/external-secrets/external-secrets/providers/v1/ovh/fake"
+)
+
+func TestValidate(t *testing.T) {
+	testCases := map[string]struct {
+		kube       kclient.Client
+		okmsClient fake.FakeOkmsClient
+		errshould  string
+	}{
+		"Error case": {
+			errshould: "failed to validate secret store: custom error",
+			okmsClient: fake.FakeOkmsClient{
+				ListSecretV2Fn: fake.NewListSecretV2Fn(errors.New("custom error")),
+			},
+		},
+		"Valid case": {
+			errshould: "",
+		},
+	}
+
+	for name, testCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			cl := ovhClient{
+				kube:       testCase.kube,
+				okmsClient: testCase.okmsClient,
+			}
+			_, err := cl.Validate()
+			if testCase.errshould != "" {
+				if err != nil && testCase.errshould != err.Error() {
+					t.Errorf("\nexpected error: %s\nactual error:   %v\n\n", testCase.errshould, err)
+				}
+				if err == nil {
+					t.Errorf("\nexpected error: %s\nactual error:   <nil>\n\n", testCase.errshould)
+				}
+			} else if err != nil {
+				t.Errorf("\nunexpected error: %v\n\n", err)
+			}
+		})
+	}
+}

+ 26 - 0
tests/__snapshot__/clustersecretstore-v1.yaml

@@ -616,6 +616,32 @@ spec:
         name: string
         namespace: string
       vault: string
+    ovh:
+      auth:
+        mtls:
+          caBundle: c3RyaW5n
+          caProvider:
+            key: string
+            name: string
+            namespace: string
+            type: "Secret" # "Secret", "ConfigMap"
+          certSecretRef:
+            key: string
+            name: string
+            namespace: string
+          keySecretRef:
+            key: string
+            name: string
+            namespace: string
+        token:
+          tokenSecretRef:
+            key: string
+            name: string
+            namespace: string
+      casRequired: true
+      okmsTimeout: 30
+      okmsid: string
+      server: string
     passbolt:
       auth:
         passwordSecretRef:

+ 26 - 0
tests/__snapshot__/secretstore-v1.yaml

@@ -616,6 +616,32 @@ spec:
         name: string
         namespace: string
       vault: string
+    ovh:
+      auth:
+        mtls:
+          caBundle: c3RyaW5n
+          caProvider:
+            key: string
+            name: string
+            namespace: string
+            type: "Secret" # "Secret", "ConfigMap"
+          certSecretRef:
+            key: string
+            name: string
+            namespace: string
+          keySecretRef:
+            key: string
+            name: string
+            namespace: string
+        token:
+          tokenSecretRef:
+            key: string
+            name: string
+            namespace: string
+      casRequired: true
+      okmsTimeout: 30
+      okmsid: string
+      server: string
     passbolt:
       auth:
         passwordSecretRef: