Browse Source

feat: Vault dynamic secrets Generator (#2074)

* feat: Vault dynamic secrets Generator

Signed-off-by: Kristián Leško <kristian.lesko@gooddata.com>

* Update pkg/provider/vault/vault.go

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

Signed-off-by: Moritz Johner <moolen@users.noreply.github.com>

* feat: Vault dynamic secrets Generator

Signed-off-by: Kristián Leško <kristian.lesko@gooddata.com>

* Update pkg/provider/vault/vault.go

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

Signed-off-by: Moritz Johner <moolen@users.noreply.github.com>

* fix: linter

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

---------

Signed-off-by: Kristián Leško <kristian.lesko@gooddata.com>
Signed-off-by: Moritz Johner <moolen@users.noreply.github.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Co-authored-by: Moritz Johner <moolen@users.noreply.github.com>
Co-authored-by: Moritz Johner <beller.moritz@googlemail.com>
Kristián Leško 3 years ago
parent
commit
1eca34c94d

+ 54 - 0
apis/generators/v1alpha1/generator_vault.go

@@ -0,0 +1,54 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+import (
+	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+type VaultDynamicSecretSpec struct {
+	// Vault API method to use (GET/POST/other)
+	Method string `json:"method,omitempty"`
+
+	// Parameters to pass to Vault write (for non-GET methods)
+	Parameters *apiextensions.JSON `json:"parameters,omitempty"`
+
+	// Vault provider common spec
+	Provider *esv1beta1.VaultProvider `json:"provider"`
+
+	// Vault path to obtain the dynamic secret from
+	Path string `json:"path"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:storageversion
+// +kubebuilder:subresource:status
+// +kubebuilder:resource:scope=Namespaced,categories={vaultdynamicsecret},shortName=vaultdynamicsecret
+type VaultDynamicSecret struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec VaultDynamicSecretSpec `json:"spec,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+type VaultDynamicSecretList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []VaultDynamicSecret `json:"items"`
+}

+ 9 - 0
apis/generators/v1alpha1/register.go

@@ -76,10 +76,19 @@ var (
 	FakeGroupVersionKind = SchemeGroupVersion.WithKind(FakeKind)
 	FakeGroupVersionKind = SchemeGroupVersion.WithKind(FakeKind)
 )
 )
 
 
+// Vault type metadata.
+var (
+	VaultDynamicSecretKind             = reflect.TypeOf(VaultDynamicSecret{}).Name()
+	VaultDynamicSecretGroupKind        = schema.GroupKind{Group: Group, Kind: VaultDynamicSecretKind}.String()
+	VaultDynamicSecretKindAPIVersion   = VaultDynamicSecretKind + "." + SchemeGroupVersion.String()
+	VaultDynamicSecretGroupVersionKind = SchemeGroupVersion.WithKind(VaultDynamicSecretKind)
+)
+
 func init() {
 func init() {
 	SchemeBuilder.Register(&ECRAuthorizationToken{}, &ECRAuthorizationToken{})
 	SchemeBuilder.Register(&ECRAuthorizationToken{}, &ECRAuthorizationToken{})
 	SchemeBuilder.Register(&GCRAccessToken{}, &GCRAccessTokenList{})
 	SchemeBuilder.Register(&GCRAccessToken{}, &GCRAccessTokenList{})
 	SchemeBuilder.Register(&ACRAccessToken{}, &ACRAccessTokenList{})
 	SchemeBuilder.Register(&ACRAccessToken{}, &ACRAccessTokenList{})
 	SchemeBuilder.Register(&Fake{}, &FakeList{})
 	SchemeBuilder.Register(&Fake{}, &FakeList{})
+	SchemeBuilder.Register(&VaultDynamicSecret{}, &VaultDynamicSecretList{})
 	SchemeBuilder.Register(&Password{}, &PasswordList{})
 	SchemeBuilder.Register(&Password{}, &PasswordList{})
 }
 }

+ 85 - 0
apis/generators/v1alpha1/zz_generated.deepcopy.go

@@ -20,7 +20,9 @@ limitations under the License.
 package v1alpha1
 package v1alpha1
 
 
 import (
 import (
+	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	"github.com/external-secrets/external-secrets/apis/meta/v1"
 	"github.com/external-secrets/external-secrets/apis/meta/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 )
 
 
@@ -635,3 +637,86 @@ func (in *PasswordSpec) DeepCopy() *PasswordSpec {
 	in.DeepCopyInto(out)
 	in.DeepCopyInto(out)
 	return out
 	return out
 }
 }
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VaultDynamicSecret) DeepCopyInto(out *VaultDynamicSecret) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultDynamicSecret.
+func (in *VaultDynamicSecret) DeepCopy() *VaultDynamicSecret {
+	if in == nil {
+		return nil
+	}
+	out := new(VaultDynamicSecret)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *VaultDynamicSecret) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VaultDynamicSecretList) DeepCopyInto(out *VaultDynamicSecretList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]VaultDynamicSecret, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultDynamicSecretList.
+func (in *VaultDynamicSecretList) DeepCopy() *VaultDynamicSecretList {
+	if in == nil {
+		return nil
+	}
+	out := new(VaultDynamicSecretList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *VaultDynamicSecretList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VaultDynamicSecretSpec) DeepCopyInto(out *VaultDynamicSecretSpec) {
+	*out = *in
+	if in.Parameters != nil {
+		in, out := &in.Parameters, &out.Parameters
+		*out = new(apiextensionsv1.JSON)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Provider != nil {
+		in, out := &in.Provider, &out.Provider
+		*out = new(v1beta1.VaultProvider)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultDynamicSecretSpec.
+func (in *VaultDynamicSecretSpec) DeepCopy() *VaultDynamicSecretSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(VaultDynamicSecretSpec)
+	in.DeepCopyInto(out)
+	return out
+}

+ 439 - 0
config/crds/bases/generators.external-secrets.io_vaultdynamicsecrets.yaml

@@ -0,0 +1,439 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.11.3
+  creationTimestamp: null
+  name: vaultdynamicsecrets.generators.external-secrets.io
+spec:
+  group: generators.external-secrets.io
+  names:
+    categories:
+    - vaultdynamicsecret
+    kind: VaultDynamicSecret
+    listKind: VaultDynamicSecretList
+    plural: vaultdynamicsecrets
+    shortNames:
+    - vaultdynamicsecret
+    singular: vaultdynamicsecret
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            properties:
+              method:
+                description: Vault API method to use (GET/POST/other)
+                type: string
+              parameters:
+                description: Parameters to pass to Vault write (for non-GET methods)
+                x-kubernetes-preserve-unknown-fields: true
+              path:
+                description: Vault path to obtain the dynamic secret from
+                type: string
+              provider:
+                description: Vault provider common spec
+                properties:
+                  auth:
+                    description: Auth configures how secret-manager authenticates
+                      with the Vault server.
+                    properties:
+                      appRole:
+                        description: AppRole authenticates with Vault using the App
+                          Role auth mechanism, with the role and secret stored in
+                          a Kubernetes Secret resource.
+                        properties:
+                          path:
+                            default: approle
+                            description: 'Path where the App Role authentication backend
+                              is mounted in Vault, e.g: "approle"'
+                            type: string
+                          roleId:
+                            description: RoleID configured in the App Role authentication
+                              backend when setting up the authentication backend in
+                              Vault.
+                            type: string
+                          secretRef:
+                            description: Reference to a key in a Secret that contains
+                              the App Role secret used to authenticate with Vault.
+                              The `key` field must be specified and denotes which
+                              entry within the Secret resource is used as the app
+                              role secret.
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                        required:
+                        - path
+                        - roleId
+                        - secretRef
+                        type: object
+                      cert:
+                        description: Cert authenticates with TLS Certificates by passing
+                          client certificate, private key and ca certificate Cert
+                          authentication method
+                        properties:
+                          clientCert:
+                            description: ClientCert is a certificate to authenticate
+                              using the Cert Vault authentication method
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                          secretRef:
+                            description: SecretRef to a key in a Secret resource containing
+                              client private key to authenticate with Vault using
+                              the Cert authentication method
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                        type: object
+                      jwt:
+                        description: Jwt authenticates with Vault by passing role
+                          and JWT token using the JWT/OIDC authentication method
+                        properties:
+                          kubernetesServiceAccountToken:
+                            description: Optional ServiceAccountToken specifies the
+                              Kubernetes service account for which to request a token
+                              for with the `TokenRequest` API.
+                            properties:
+                              audiences:
+                                description: 'Optional audiences field that will be
+                                  used to request a temporary Kubernetes service account
+                                  token for the service account referenced by `serviceAccountRef`.
+                                  Defaults to a single audience `vault` it not specified.
+                                  Deprecated: use serviceAccountRef.Audiences instead'
+                                items:
+                                  type: string
+                                type: array
+                              expirationSeconds:
+                                description: 'Optional expiration time in seconds
+                                  that will be used to request a temporary Kubernetes
+                                  service account token for the service account referenced
+                                  by `serviceAccountRef`. Deprecated: this will be
+                                  removed in the future. Defaults to 10 minutes.'
+                                format: int64
+                                type: integer
+                              serviceAccountRef:
+                                description: Service account field containing the
+                                  name of a kubernetes ServiceAccount.
+                                properties:
+                                  audiences:
+                                    description: Audience specifies the `aud` claim
+                                      for the service account token If the service
+                                      account uses a well-known annotation for e.g.
+                                      IRSA or GCP Workload Identity then this audiences
+                                      will be appended to the list
+                                    items:
+                                      type: string
+                                    type: array
+                                  name:
+                                    description: The name of the ServiceAccount resource
+                                      being referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                required:
+                                - name
+                                type: object
+                            required:
+                            - serviceAccountRef
+                            type: object
+                          path:
+                            default: jwt
+                            description: 'Path where the JWT authentication backend
+                              is mounted in Vault, e.g: "jwt"'
+                            type: string
+                          role:
+                            description: Role is a JWT role to authenticate using
+                              the JWT/OIDC Vault authentication method
+                            type: string
+                          secretRef:
+                            description: Optional SecretRef that refers to a key in
+                              a Secret resource containing JWT token to authenticate
+                              with Vault using the JWT/OIDC authentication method.
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                        required:
+                        - path
+                        type: object
+                      kubernetes:
+                        description: Kubernetes authenticates with Vault by passing
+                          the ServiceAccount token stored in the named Secret resource
+                          to the Vault server.
+                        properties:
+                          mountPath:
+                            default: kubernetes
+                            description: 'Path where the Kubernetes authentication
+                              backend is mounted in Vault, e.g: "kubernetes"'
+                            type: string
+                          role:
+                            description: A required field containing the Vault Role
+                              to assume. A Role binds a Kubernetes ServiceAccount
+                              with a set of Vault policies.
+                            type: string
+                          secretRef:
+                            description: Optional secret field containing a Kubernetes
+                              ServiceAccount JWT used for authenticating with Vault.
+                              If a name is specified without a key, `token` is the
+                              default. If one is not specified, the one bound to the
+                              controller will be used.
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                          serviceAccountRef:
+                            description: Optional service account field containing
+                              the name of a kubernetes ServiceAccount. If the service
+                              account is specified, the service account secret token
+                              JWT will be used for authenticating with Vault. If the
+                              service account selector is not supplied, the secretRef
+                              will be used instead.
+                            properties:
+                              audiences:
+                                description: Audience specifies the `aud` claim for
+                                  the service account token If the service account
+                                  uses a well-known annotation for e.g. IRSA or GCP
+                                  Workload Identity then this audiences will be appended
+                                  to the list
+                                items:
+                                  type: string
+                                type: array
+                              name:
+                                description: The name of the ServiceAccount resource
+                                  being referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            required:
+                            - name
+                            type: object
+                        required:
+                        - mountPath
+                        - role
+                        type: object
+                      ldap:
+                        description: Ldap authenticates with Vault by passing username/password
+                          pair using the LDAP authentication method
+                        properties:
+                          path:
+                            default: ldap
+                            description: 'Path where the LDAP authentication backend
+                              is mounted in Vault, e.g: "ldap"'
+                            type: string
+                          secretRef:
+                            description: SecretRef to a key in a Secret resource containing
+                              password for the LDAP user used to authenticate with
+                              Vault using the LDAP authentication method
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                          username:
+                            description: Username is a LDAP user name used to authenticate
+                              using the LDAP Vault authentication method
+                            type: string
+                        required:
+                        - path
+                        - username
+                        type: object
+                      tokenSecretRef:
+                        description: TokenSecretRef authenticates with Vault by presenting
+                          a token.
+                        properties:
+                          key:
+                            description: The key of the entry in the Secret resource's
+                              `data` field to be used. Some instances of this field
+                              may be defaulted, in others it may be required.
+                            type: string
+                          name:
+                            description: The name of the Secret resource being referred
+                              to.
+                            type: string
+                          namespace:
+                            description: Namespace of the resource being referred
+                              to. Ignored if referent is not cluster-scoped. cluster-scoped
+                              defaults to the namespace of the referent.
+                            type: string
+                        type: object
+                    type: object
+                  caBundle:
+                    description: PEM encoded CA bundle used to validate Vault server
+                      certificate. Only used if the Server URL is using HTTPS protocol.
+                      This parameter is ignored for plain HTTP protocol connection.
+                      If not set the system root certificates are used to validate
+                      the TLS connection.
+                    format: byte
+                    type: string
+                  caProvider:
+                    description: The provider for the CA bundle to use to validate
+                      Vault server certificate.
+                    properties:
+                      key:
+                        description: The key where the CA certificate can be found
+                          in the Secret or ConfigMap.
+                        type: string
+                      name:
+                        description: The name of the object located at the provider
+                          type.
+                        type: string
+                      namespace:
+                        description: The namespace the Provider type is in. Can only
+                          be defined when used in a ClusterSecretStore.
+                        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
+                  forwardInconsistent:
+                    description: ForwardInconsistent tells Vault to forward read-after-write
+                      requests to the Vault leader instead of simply retrying within
+                      a loop. This can increase performance if the option is enabled
+                      serverside. https://www.vaultproject.io/docs/configuration/replication#allow_forwarding_via_header
+                    type: boolean
+                  namespace:
+                    description: 'Name of the vault namespace. Namespaces is a set
+                      of features within Vault Enterprise that allows Vault environments
+                      to support Secure Multi-tenancy. e.g: "ns1". More about namespaces
+                      can be found here https://www.vaultproject.io/docs/enterprise/namespaces'
+                    type: string
+                  path:
+                    description: 'Path is the mount path of the Vault KV backend endpoint,
+                      e.g: "secret". The v2 KV secret engine version specific "/data"
+                      path suffix for fetching secrets from Vault is optional and
+                      will be appended if not present in specified path.'
+                    type: string
+                  readYourWrites:
+                    description: ReadYourWrites ensures isolated read-after-write
+                      semantics by providing discovered cluster replication states
+                      in each request. More information about eventual consistency
+                      in Vault can be found here https://www.vaultproject.io/docs/enterprise/consistency
+                    type: boolean
+                  server:
+                    description: 'Server is the connection address for the Vault server,
+                      e.g: "https://vault.example.com:8200".'
+                    type: string
+                  version:
+                    default: v2
+                    description: Version is the Vault KV secret engine version. This
+                      can be either "v1" or "v2". Version defaults to "v2".
+                    enum:
+                    - v1
+                    - v2
+                    type: string
+                required:
+                - auth
+                - server
+                type: object
+            required:
+            - path
+            - provider
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}

+ 4 - 3
deploy/charts/external-secrets/templates/rbac.yaml

@@ -49,11 +49,12 @@ rules:
   - apiGroups:
   - apiGroups:
     - "generators.external-secrets.io"
     - "generators.external-secrets.io"
     resources:
     resources:
-    - "fakes"
-    - "passwords"
     - "acraccesstokens"
     - "acraccesstokens"
-    - "gcraccesstokens"
     - "ecrauthorizationtokens"
     - "ecrauthorizationtokens"
+    - "fakes"
+    - "gcraccesstokens"
+    - "passwords"
+    - "vaultdynamicsecrets"
     verbs:
     verbs:
     - "get"
     - "get"
     - "list"
     - "list"

+ 322 - 0
deploy/crds/bundle.yaml

@@ -6866,3 +6866,325 @@ spec:
           name: kubernetes
           name: kubernetes
           namespace: default
           namespace: default
           path: /convert
           path: /convert
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.11.3
+  creationTimestamp: null
+  name: vaultdynamicsecrets.generators.external-secrets.io
+spec:
+  group: generators.external-secrets.io
+  names:
+    categories:
+      - vaultdynamicsecret
+    kind: VaultDynamicSecret
+    listKind: VaultDynamicSecretList
+    plural: vaultdynamicsecrets
+    shortNames:
+      - vaultdynamicsecret
+    singular: vaultdynamicsecret
+  scope: Namespaced
+  versions:
+    - name: v1alpha1
+      schema:
+        openAPIV3Schema:
+          properties:
+            apiVersion:
+              description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+              type: string
+            kind:
+              description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+              type: string
+            metadata:
+              type: object
+            spec:
+              properties:
+                method:
+                  description: Vault API method to use (GET/POST/other)
+                  type: string
+                parameters:
+                  description: Parameters to pass to Vault write (for non-GET methods)
+                  x-kubernetes-preserve-unknown-fields: true
+                path:
+                  description: Vault path to obtain the dynamic secret from
+                  type: string
+                provider:
+                  description: Vault provider common spec
+                  properties:
+                    auth:
+                      description: Auth configures how secret-manager authenticates with the Vault server.
+                      properties:
+                        appRole:
+                          description: AppRole authenticates with Vault using the App Role auth mechanism, with the role and secret stored in a Kubernetes Secret resource.
+                          properties:
+                            path:
+                              default: approle
+                              description: 'Path where the App Role authentication backend is mounted in Vault, e.g: "approle"'
+                              type: string
+                            roleId:
+                              description: RoleID configured in the App Role authentication backend when setting up the authentication backend in Vault.
+                              type: string
+                            secretRef:
+                              description: Reference to a key in a Secret that contains the App Role secret used to authenticate with Vault. The `key` field must be specified and denotes which entry within the Secret resource is used as the app role secret.
+                              properties:
+                                key:
+                                  description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              type: object
+                          required:
+                            - path
+                            - roleId
+                            - secretRef
+                          type: object
+                        cert:
+                          description: Cert authenticates with TLS Certificates by passing client certificate, private key and ca certificate Cert authentication method
+                          properties:
+                            clientCert:
+                              description: ClientCert is a certificate to authenticate using the Cert Vault authentication method
+                              properties:
+                                key:
+                                  description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              type: object
+                            secretRef:
+                              description: SecretRef to a key in a Secret resource containing client private key to authenticate with Vault using the Cert authentication method
+                              properties:
+                                key:
+                                  description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              type: object
+                          type: object
+                        jwt:
+                          description: Jwt authenticates with Vault by passing role and JWT token using the JWT/OIDC authentication method
+                          properties:
+                            kubernetesServiceAccountToken:
+                              description: Optional ServiceAccountToken specifies the Kubernetes service account for which to request a token for with the `TokenRequest` API.
+                              properties:
+                                audiences:
+                                  description: 'Optional audiences field that will be used to request a temporary Kubernetes service account token for the service account referenced by `serviceAccountRef`. Defaults to a single audience `vault` it not specified. Deprecated: use serviceAccountRef.Audiences instead'
+                                  items:
+                                    type: string
+                                  type: array
+                                expirationSeconds:
+                                  description: 'Optional expiration time in seconds that will be used to request a temporary Kubernetes service account token for the service account referenced by `serviceAccountRef`. Deprecated: this will be removed in the future. Defaults to 10 minutes.'
+                                  format: int64
+                                  type: integer
+                                serviceAccountRef:
+                                  description: Service account field containing the name of a kubernetes ServiceAccount.
+                                  properties:
+                                    audiences:
+                                      description: Audience specifies the `aud` claim for the service account token If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity then this audiences will be appended to the list
+                                      items:
+                                        type: string
+                                      type: array
+                                    name:
+                                      description: The name of the ServiceAccount resource being referred to.
+                                      type: string
+                                    namespace:
+                                      description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                      type: string
+                                  required:
+                                    - name
+                                  type: object
+                              required:
+                                - serviceAccountRef
+                              type: object
+                            path:
+                              default: jwt
+                              description: 'Path where the JWT authentication backend is mounted in Vault, e.g: "jwt"'
+                              type: string
+                            role:
+                              description: Role is a JWT role to authenticate using the JWT/OIDC Vault authentication method
+                              type: string
+                            secretRef:
+                              description: Optional SecretRef that refers to a key in a Secret resource containing JWT token to authenticate with Vault using the JWT/OIDC authentication method.
+                              properties:
+                                key:
+                                  description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              type: object
+                          required:
+                            - path
+                          type: object
+                        kubernetes:
+                          description: Kubernetes authenticates with Vault by passing the ServiceAccount token stored in the named Secret resource to the Vault server.
+                          properties:
+                            mountPath:
+                              default: kubernetes
+                              description: 'Path where the Kubernetes authentication backend is mounted in Vault, e.g: "kubernetes"'
+                              type: string
+                            role:
+                              description: A required field containing the Vault Role to assume. A Role binds a Kubernetes ServiceAccount with a set of Vault policies.
+                              type: string
+                            secretRef:
+                              description: Optional secret field containing a Kubernetes ServiceAccount JWT used for authenticating with Vault. If a name is specified without a key, `token` is the default. If one is not specified, the one bound to the controller will be used.
+                              properties:
+                                key:
+                                  description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              type: object
+                            serviceAccountRef:
+                              description: Optional service account field containing the name of a kubernetes ServiceAccount. If the service account is specified, the service account secret token JWT will be used for authenticating with Vault. If the service account selector is not supplied, the secretRef will be used instead.
+                              properties:
+                                audiences:
+                                  description: Audience specifies the `aud` claim for the service account token If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity then this audiences will be appended to the list
+                                  items:
+                                    type: string
+                                  type: array
+                                name:
+                                  description: The name of the ServiceAccount resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              required:
+                                - name
+                              type: object
+                          required:
+                            - mountPath
+                            - role
+                          type: object
+                        ldap:
+                          description: Ldap authenticates with Vault by passing username/password pair using the LDAP authentication method
+                          properties:
+                            path:
+                              default: ldap
+                              description: 'Path where the LDAP authentication backend is mounted in Vault, e.g: "ldap"'
+                              type: string
+                            secretRef:
+                              description: SecretRef to a key in a Secret resource containing password for the LDAP user used to authenticate with Vault using the LDAP authentication method
+                              properties:
+                                key:
+                                  description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              type: object
+                            username:
+                              description: Username is a LDAP user name used to authenticate using the LDAP Vault authentication method
+                              type: string
+                          required:
+                            - path
+                            - username
+                          type: object
+                        tokenSecretRef:
+                          description: TokenSecretRef authenticates with Vault by presenting a token.
+                          properties:
+                            key:
+                              description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                              type: string
+                            name:
+                              description: The name of the Secret resource being referred to.
+                              type: string
+                            namespace:
+                              description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                              type: string
+                          type: object
+                      type: object
+                    caBundle:
+                      description: PEM encoded CA bundle used to validate Vault server certificate. Only used if the Server URL is using HTTPS protocol. This parameter is ignored for plain HTTP protocol connection. If not set the system root certificates are used to validate the TLS connection.
+                      format: byte
+                      type: string
+                    caProvider:
+                      description: The provider for the CA bundle to use to validate Vault server certificate.
+                      properties:
+                        key:
+                          description: The key where the CA certificate can be found in the Secret or ConfigMap.
+                          type: string
+                        name:
+                          description: The name of the object located at the provider type.
+                          type: string
+                        namespace:
+                          description: The namespace the Provider type is in. Can only be defined when used in a ClusterSecretStore.
+                          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
+                    forwardInconsistent:
+                      description: ForwardInconsistent tells Vault to forward read-after-write requests to the Vault leader instead of simply retrying within a loop. This can increase performance if the option is enabled serverside. https://www.vaultproject.io/docs/configuration/replication#allow_forwarding_via_header
+                      type: boolean
+                    namespace:
+                      description: 'Name of the vault namespace. Namespaces is a set of features within Vault Enterprise that allows Vault environments to support Secure Multi-tenancy. e.g: "ns1". More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces'
+                      type: string
+                    path:
+                      description: 'Path is the mount path of the Vault KV backend endpoint, e.g: "secret". The v2 KV secret engine version specific "/data" path suffix for fetching secrets from Vault is optional and will be appended if not present in specified path.'
+                      type: string
+                    readYourWrites:
+                      description: ReadYourWrites ensures isolated read-after-write semantics by providing discovered cluster replication states in each request. More information about eventual consistency in Vault can be found here https://www.vaultproject.io/docs/enterprise/consistency
+                      type: boolean
+                    server:
+                      description: 'Server is the connection address for the Vault server, e.g: "https://vault.example.com:8200".'
+                      type: string
+                    version:
+                      default: v2
+                      description: Version is the Vault KV secret engine version. This can be either "v1" or "v2". Version defaults to "v2".
+                      enum:
+                        - v1
+                        - v2
+                      type: string
+                  required:
+                    - auth
+                    - server
+                  type: object
+              required:
+                - path
+                - provider
+              type: object
+          type: object
+      served: true
+      storage: true
+      subresources:
+        status: {}
+  conversion:
+    strategy: Webhook
+    webhook:
+      conversionReviewVersions:
+        - v1
+      clientConfig:
+        service:
+          name: kubernetes
+          namespace: default
+          path: /convert

+ 0 - 2
pkg/controllers/clusterexternalsecret/suite_test.go

@@ -16,7 +16,6 @@ package clusterexternalsecret
 
 
 import (
 import (
 	"context"
 	"context"
-	"math/rand"
 	"path/filepath"
 	"path/filepath"
 	"testing"
 	"testing"
 	"time"
 	"time"
@@ -50,7 +49,6 @@ func TestAPIs(t *testing.T) {
 }
 }
 
 
 var _ = BeforeSuite(func() {
 var _ = BeforeSuite(func() {
-	rand.Seed(time.Now().UnixNano())
 	log := zap.New(zap.WriteTo(GinkgoWriter), zap.Level(zapcore.DebugLevel))
 	log := zap.New(zap.WriteTo(GinkgoWriter), zap.Level(zapcore.DebugLevel))
 
 
 	logf.SetLogger(log)
 	logf.SetLogger(log)

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

@@ -12,14 +12,15 @@ See the License for the specific language governing permissions and
 limitations under the License.
 limitations under the License.
 */
 */
 
 
+//nolint:revive
 package register
 package register
 
 
 // packages imported here are registered to the controller schema.
 // packages imported here are registered to the controller schema.
-//nolint:revive
 import (
 import (
 	_ "github.com/external-secrets/external-secrets/pkg/generator/acr"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/acr"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/ecr"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/ecr"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/fake"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/fake"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/gcr"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/gcr"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/password"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/password"
+	_ "github.com/external-secrets/external-secrets/pkg/generator/vault"
 )
 )

+ 117 - 0
pkg/generator/vault/vault.go

@@ -0,0 +1,117 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package vaultdynamic
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	vault "github.com/hashicorp/vault/api"
+	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/client-go/kubernetes"
+	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
+	"sigs.k8s.io/yaml"
+
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+	provider "github.com/external-secrets/external-secrets/pkg/provider/vault"
+)
+
+type Generator struct{}
+
+const (
+	errNoSpec      = "no config spec provided"
+	errParseSpec   = "unable to parse spec: %w"
+	errVaultClient = "unable to setup Vault client: %w"
+	errGetSecret   = "unable to get dynamic secret: %w"
+)
+
+func (g *Generator) Generate(ctx context.Context, jsonSpec *apiextensions.JSON, kube client.Client, namespace string) (map[string][]byte, error) {
+	c := &provider.Connector{NewVaultClient: provider.NewVaultClient}
+
+	// controller-runtime/client does not support TokenRequest or other subresource APIs
+	// so we need to construct our own client and use it to fetch tokens
+	// (for Kubernetes service account token auth)
+	restCfg, err := ctrlcfg.GetConfig()
+	if err != nil {
+		return nil, err
+	}
+	clientset, err := kubernetes.NewForConfig(restCfg)
+	if err != nil {
+		return nil, err
+	}
+
+	return g.generate(ctx, c, jsonSpec, kube, clientset.CoreV1(), namespace)
+}
+
+func (g *Generator) generate(ctx context.Context, c *provider.Connector, jsonSpec *apiextensions.JSON, kube client.Client, corev1 typedcorev1.CoreV1Interface, namespace string) (map[string][]byte, error) {
+	if jsonSpec == nil {
+		return nil, fmt.Errorf(errNoSpec)
+	}
+	res, err := parseSpec(jsonSpec.Raw)
+	if err != nil {
+		return nil, fmt.Errorf(errParseSpec, err)
+	}
+	if res == nil || res.Spec.Provider == nil {
+		return nil, fmt.Errorf("no Vault provider config in spec")
+	}
+	cl, err := c.NewGeneratorClient(ctx, kube, corev1, res.Spec.Provider, namespace)
+	if err != nil {
+		return nil, fmt.Errorf(errVaultClient, err)
+	}
+
+	var result *vault.Secret
+	if res.Spec.Method == "" || res.Spec.Method == "GET" {
+		result, err = cl.Logical().ReadWithDataWithContext(ctx, res.Spec.Path, nil)
+	} else if res.Spec.Method == "LIST" {
+		result, err = cl.Logical().ListWithContext(ctx, res.Spec.Path)
+	} else if res.Spec.Method == "DELETE" {
+		result, err = cl.Logical().DeleteWithContext(ctx, res.Spec.Path)
+	} else {
+		params := make(map[string]interface{})
+		err = json.Unmarshal(res.Spec.Parameters.Raw, &params)
+		if err != nil {
+			return nil, err
+		}
+		result, err = cl.Logical().WriteWithContext(ctx, res.Spec.Path, params)
+	}
+	if err != nil {
+		return nil, err
+	}
+	if result == nil {
+		return nil, fmt.Errorf(errGetSecret, fmt.Errorf("empty response from Vault"))
+	}
+
+	response := make(map[string][]byte)
+	for k := range result.Data {
+		response[k], err = provider.GetTypedKey(result.Data, k)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return response, nil
+}
+
+func parseSpec(data []byte) (*genv1alpha1.VaultDynamicSecret, error) {
+	var spec genv1alpha1.VaultDynamicSecret
+	err := yaml.Unmarshal(data, &spec)
+	return &spec, err
+}
+
+func init() {
+	genv1alpha1.Register(genv1alpha1.VaultDynamicSecretKind, &Generator{})
+}

+ 145 - 0
pkg/generator/vault/vault_test.go

@@ -0,0 +1,145 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package vaultdynamic
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	corev1 "k8s.io/api/core/v1"
+	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	utilfake "github.com/external-secrets/external-secrets/pkg/provider/util/fake"
+	provider "github.com/external-secrets/external-secrets/pkg/provider/vault"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/fake"
+)
+
+type args struct {
+	jsonSpec *apiextensions.JSON
+	kube     kclient.Client
+	corev1   typedcorev1.CoreV1Interface
+}
+
+type want struct {
+	val map[string][]byte
+	err error
+}
+
+type testCase struct {
+	reason string
+	args   args
+	want   want
+}
+
+func TestVaultDynamicSecretGenerator(t *testing.T) {
+	cases := map[string]testCase{
+		"NilSpec": {
+			reason: "Raise an error with empty spec.",
+			args: args{
+				jsonSpec: nil,
+			},
+			want: want{
+				err: errors.New("no config spec provided"),
+			},
+		},
+		"InvalidSpec": {
+			reason: "Raise an error with invalid spec.",
+			args: args{
+				jsonSpec: &apiextensions.JSON{
+					Raw: []byte(``),
+				},
+			},
+			want: want{
+				err: errors.New("no Vault provider config in spec"),
+			},
+		},
+		"MissingRoleName": {
+			reason: "Raise error if incomplete k8s auth config is provided.",
+			args: args{
+				corev1: utilfake.NewCreateTokenMock().WithToken("ok"),
+				jsonSpec: &apiextensions.JSON{
+					Raw: []byte(`apiVersion: generators.external-secrets.io/v1alpha1
+kind: VaultDynamicSecret
+spec:
+  provider:
+    auth:
+      kubernetes:
+        serviceAccountRef:
+          name: "testing"
+  method: GET
+  path: "github/token/example"`),
+				},
+				kube: clientfake.NewClientBuilder().Build(),
+			},
+			want: want{
+				err: fmt.Errorf("unable to setup Vault client: no role name was provided"),
+			},
+		},
+		"EmptyVaultResponse": {
+			reason: "Fail on empty response from Vault.",
+			args: args{
+				corev1: utilfake.NewCreateTokenMock().WithToken("ok"),
+				jsonSpec: &apiextensions.JSON{
+					Raw: []byte(`apiVersion: generators.external-secrets.io/v1alpha1
+kind: VaultDynamicSecret
+spec:
+  provider:
+    auth:
+      kubernetes:
+        role: test
+        serviceAccountRef:
+          name: "testing"
+  method: GET
+  path: "github/token/example"`),
+				},
+				kube: clientfake.NewClientBuilder().WithObjects(&corev1.ServiceAccount{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "testing",
+						Namespace: "testing",
+					},
+					Secrets: []corev1.ObjectReference{
+						{
+							Name: "test",
+						},
+					},
+				}).Build(),
+			},
+			want: want{
+				err: fmt.Errorf("unable to get dynamic secret: empty response from Vault"),
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			c := &provider.Connector{NewVaultClient: fake.ClientWithLoginMock}
+			gen := &Generator{}
+			val, err := gen.generate(context.Background(), c, tc.args.jsonSpec, tc.args.kube, tc.args.corev1, "testing")
+			if diff := cmp.Diff(tc.want.err.Error(), err.Error()); diff != "" {
+				t.Errorf("\n%s\nvault.GetSecret(...): -want error, +got error:\n%s", tc.reason, diff)
+			}
+			if diff := cmp.Diff(tc.want.val, val); diff != "" {
+				t.Errorf("\n%s\nvault.GetSecret(...): -want val, +got val:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}

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

@@ -12,10 +12,10 @@ See the License for the specific language governing permissions and
 limitations under the License.
 limitations under the License.
 */
 */
 
 
+//nolint:revive
 package register
 package register
 
 
 // packages imported here are registered to the controller schema.
 // packages imported here are registered to the controller schema.
-//nolint:revive
 import (
 import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/akeyless"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/akeyless"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/alibaba"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/alibaba"

+ 26 - 0
pkg/provider/vault/fake/vault.go

@@ -18,6 +18,8 @@ import (
 	"context"
 	"context"
 
 
 	vault "github.com/hashicorp/vault/api"
 	vault "github.com/hashicorp/vault/api"
+
+	util "github.com/external-secrets/external-secrets/pkg/provider/vault/util"
 )
 )
 
 
 type LoginFn func(ctx context.Context, authMethod vault.AuthMethod) (*vault.Secret, error)
 type LoginFn func(ctx context.Context, authMethod vault.AuthMethod) (*vault.Secret, error)
@@ -235,3 +237,27 @@ func (c *VaultClient) SetNamespace(namespace string) {
 func (c *VaultClient) AddHeader(key, value string) {
 func (c *VaultClient) AddHeader(key, value string) {
 	c.MockAddHeader(key, value)
 	c.MockAddHeader(key, value)
 }
 }
+
+func ClientWithLoginMock(c *vault.Config) (util.Client, error) {
+	cl := VaultClient{
+		MockAuthToken: NewAuthTokenFn(),
+		MockSetToken:  NewSetTokenFn(),
+		MockToken:     NewTokenFn(""),
+		MockAuth:      NewVaultAuth(),
+		MockLogical:   NewVaultLogical(),
+	}
+	auth := cl.Auth()
+	token := cl.AuthToken()
+	logical := cl.Logical()
+	out := util.VClient{
+		SetTokenFunc:     cl.SetToken,
+		TokenFunc:        cl.Token,
+		ClearTokenFunc:   cl.ClearToken,
+		AuthField:        auth,
+		AuthTokenField:   token,
+		LogicalField:     logical,
+		SetNamespaceFunc: cl.SetNamespace,
+		AddHeaderFunc:    cl.AddHeader,
+	}
+	return out, nil
+}

+ 91 - 0
pkg/provider/vault/util/vault.go

@@ -0,0 +1,91 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package util
+
+import (
+	"context"
+
+	vault "github.com/hashicorp/vault/api"
+)
+
+type Auth interface {
+	Login(ctx context.Context, authMethod vault.AuthMethod) (*vault.Secret, error)
+}
+
+type Token interface {
+	RevokeSelfWithContext(ctx context.Context, token string) error
+	LookupSelfWithContext(ctx context.Context) (*vault.Secret, error)
+}
+
+type Logical interface {
+	ReadWithDataWithContext(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error)
+	ListWithContext(ctx context.Context, path string) (*vault.Secret, error)
+	WriteWithContext(ctx context.Context, path string, data map[string]interface{}) (*vault.Secret, error)
+	DeleteWithContext(ctx context.Context, path string) (*vault.Secret, error)
+}
+
+type Client interface {
+	SetToken(v string)
+	Token() string
+	ClearToken()
+	Auth() Auth
+	Logical() Logical
+	AuthToken() Token
+	SetNamespace(namespace string)
+	AddHeader(key, value string)
+}
+
+type VClient struct {
+	SetTokenFunc     func(v string)
+	TokenFunc        func() string
+	ClearTokenFunc   func()
+	AuthField        Auth
+	LogicalField     Logical
+	AuthTokenField   Token
+	SetNamespaceFunc func(namespace string)
+	AddHeaderFunc    func(key, value string)
+}
+
+func (v VClient) AddHeader(key, value string) {
+	v.AddHeaderFunc(key, value)
+}
+
+func (v VClient) SetNamespace(namespace string) {
+	v.SetNamespaceFunc(namespace)
+}
+
+func (v VClient) ClearToken() {
+	v.ClearTokenFunc()
+}
+
+func (v VClient) Token() string {
+	return v.TokenFunc()
+}
+
+func (v VClient) SetToken(token string) {
+	v.SetTokenFunc(token)
+}
+
+func (v VClient) Auth() Auth {
+	return v.AuthField
+}
+
+func (v VClient) AuthToken() Token {
+	return v.AuthTokenField
+}
+
+func (v VClient) Logical() Logical {
+	return v.LogicalField
+}

+ 83 - 115
pkg/provider/vault/vault.go

@@ -24,6 +24,7 @@ import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
+	"reflect"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 
 
@@ -50,15 +51,16 @@ import (
 	"github.com/external-secrets/external-secrets/pkg/feature"
 	"github.com/external-secrets/external-secrets/pkg/feature"
 	"github.com/external-secrets/external-secrets/pkg/find"
 	"github.com/external-secrets/external-secrets/pkg/find"
 	"github.com/external-secrets/external-secrets/pkg/provider/metrics"
 	"github.com/external-secrets/external-secrets/pkg/provider/metrics"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/util"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 )
 
 
 var (
 var (
-	_           esv1beta1.Provider      = &connector{}
+	_           esv1beta1.Provider      = &Connector{}
 	_           esv1beta1.SecretsClient = &client{}
 	_           esv1beta1.SecretsClient = &client{}
 	enableCache bool
 	enableCache bool
 	logger      = ctrl.Log.WithName("provider").WithName("vault")
 	logger      = ctrl.Log.WithName("provider").WithName("vault")
-	clientCache *cache.Cache[Client]
+	clientCache *cache.Cache[util.Client]
 )
 )
 
 
 const (
 const (
@@ -76,7 +78,7 @@ const (
 	errDataField                    = "failed to find data field"
 	errDataField                    = "failed to find data field"
 	errJSONUnmarshall               = "failed to unmarshall JSON"
 	errJSONUnmarshall               = "failed to unmarshall JSON"
 	errPathInvalid                  = "provided Path isn't a valid kv v2 path"
 	errPathInvalid                  = "provided Path isn't a valid kv v2 path"
-	errSecretFormat                 = "secret data not in expected format"
+	errSecretFormat                 = "secret data for property %s not in expected format: %s"
 	errUnexpectedKey                = "unexpected key in data: %s"
 	errUnexpectedKey                = "unexpected key in data: %s"
 	errVaultToken                   = "cannot parse Vault authentication token: %w"
 	errVaultToken                   = "cannot parse Vault authentication token: %w"
 	errVaultRequest                 = "error from Vault request: %w"
 	errVaultRequest                 = "error from Vault request: %w"
@@ -119,92 +121,22 @@ const (
 
 
 // https://github.com/external-secrets/external-secrets/issues/644
 // https://github.com/external-secrets/external-secrets/issues/644
 var _ esv1beta1.SecretsClient = &client{}
 var _ esv1beta1.SecretsClient = &client{}
-var _ esv1beta1.Provider = &connector{}
-
-type Auth interface {
-	Login(ctx context.Context, authMethod vault.AuthMethod) (*vault.Secret, error)
-}
-
-type Token interface {
-	RevokeSelfWithContext(ctx context.Context, token string) error
-	LookupSelfWithContext(ctx context.Context) (*vault.Secret, error)
-}
-
-type Logical interface {
-	ReadWithDataWithContext(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error)
-	ListWithContext(ctx context.Context, path string) (*vault.Secret, error)
-	WriteWithContext(ctx context.Context, path string, data map[string]interface{}) (*vault.Secret, error)
-	DeleteWithContext(ctx context.Context, path string) (*vault.Secret, error)
-}
-
-type Client interface {
-	SetToken(v string)
-	Token() string
-	ClearToken()
-	Auth() Auth
-	Logical() Logical
-	AuthToken() Token
-	SetNamespace(namespace string)
-	AddHeader(key, value string)
-}
-
-type VClient struct {
-	setToken     func(v string)
-	token        func() string
-	clearToken   func()
-	auth         Auth
-	logical      Logical
-	authToken    Token
-	setNamespace func(namespace string)
-	addHeader    func(key, value string)
-}
-
-func (v VClient) AddHeader(key, value string) {
-	v.addHeader(key, value)
-}
-
-func (v VClient) SetNamespace(namespace string) {
-	v.setNamespace(namespace)
-}
-
-func (v VClient) ClearToken() {
-	v.clearToken()
-}
-
-func (v VClient) Token() string {
-	return v.token()
-}
-
-func (v VClient) SetToken(token string) {
-	v.setToken(token)
-}
-
-func (v VClient) Auth() Auth {
-	return v.auth
-}
-
-func (v VClient) AuthToken() Token {
-	return v.authToken
-}
-
-func (v VClient) Logical() Logical {
-	return v.logical
-}
+var _ esv1beta1.Provider = &Connector{}
 
 
 type client struct {
 type client struct {
 	kube      kclient.Client
 	kube      kclient.Client
 	store     *esv1beta1.VaultProvider
 	store     *esv1beta1.VaultProvider
 	log       logr.Logger
 	log       logr.Logger
 	corev1    typedcorev1.CoreV1Interface
 	corev1    typedcorev1.CoreV1Interface
-	client    Client
-	auth      Auth
-	logical   Logical
-	token     Token
+	client    util.Client
+	auth      util.Auth
+	logical   util.Logical
+	token     util.Token
 	namespace string
 	namespace string
 	storeKind string
 	storeKind string
 }
 }
 
 
-func newVaultClient(c *vault.Config) (Client, error) {
+func NewVaultClient(c *vault.Config) (util.Client, error) {
 	cl, err := vault.NewClient(c)
 	cl, err := vault.NewClient(c)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -212,20 +144,20 @@ func newVaultClient(c *vault.Config) (Client, error) {
 	auth := cl.Auth()
 	auth := cl.Auth()
 	logical := cl.Logical()
 	logical := cl.Logical()
 	token := cl.Auth().Token()
 	token := cl.Auth().Token()
-	out := VClient{
-		setToken:     cl.SetToken,
-		token:        cl.Token,
-		clearToken:   cl.ClearToken,
-		auth:         auth,
-		authToken:    token,
-		logical:      logical,
-		setNamespace: cl.SetNamespace,
-		addHeader:    cl.AddHeader,
-	}
-	return out, nil
+	out := util.VClient{
+		SetTokenFunc:     cl.SetToken,
+		TokenFunc:        cl.Token,
+		ClearTokenFunc:   cl.ClearToken,
+		AuthField:        auth,
+		AuthTokenField:   token,
+		LogicalField:     logical,
+		SetNamespaceFunc: cl.SetNamespace,
+		AddHeaderFunc:    cl.AddHeader,
+	}
+	return &out, nil
 }
 }
 
 
-func getVaultClient(c *connector, store esv1beta1.GenericStore, cfg *vault.Config) (Client, error) {
+func getVaultClient(c *Connector, store esv1beta1.GenericStore, cfg *vault.Config) (util.Client, error) {
 	isStaticToken := store.GetSpec().Provider.Vault.Auth.TokenSecretRef != nil
 	isStaticToken := store.GetSpec().Provider.Vault.Auth.TokenSecretRef != nil
 	useCache := enableCache && !isStaticToken
 	useCache := enableCache && !isStaticToken
 
 
@@ -241,7 +173,7 @@ func getVaultClient(c *connector, store esv1beta1.GenericStore, cfg *vault.Confi
 		}
 		}
 	}
 	}
 
 
-	client, err := c.newVaultClient(cfg)
+	client, err := c.NewVaultClient(cfg)
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf(errVaultClient, err)
 		return nil, fmt.Errorf(errVaultClient, err)
 	}
 	}
@@ -252,16 +184,15 @@ func getVaultClient(c *connector, store esv1beta1.GenericStore, cfg *vault.Confi
 	return client, nil
 	return client, nil
 }
 }
 
 
-type connector struct {
-	newVaultClient func(c *vault.Config) (Client, error)
+type Connector struct {
+	NewVaultClient func(c *vault.Config) (util.Client, error)
 }
 }
 
 
 // Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
 // Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
-func (c *connector) Capabilities() esv1beta1.SecretStoreCapabilities {
+func (c *Connector) Capabilities() esv1beta1.SecretStoreCapabilities {
 	return esv1beta1.SecretStoreReadWrite
 	return esv1beta1.SecretStoreReadWrite
 }
 }
-
-func (c *connector) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
+func (c *Connector) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	// controller-runtime/client does not support TokenRequest or other subresource APIs
 	// controller-runtime/client does not support TokenRequest or other subresource APIs
 	// so we need to construct our own client and use it to fetch tokens
 	// so we need to construct our own client and use it to fetch tokens
 	// (for Kubernetes service account token auth)
 	// (for Kubernetes service account token auth)
@@ -277,32 +208,63 @@ func (c *connector) NewClient(ctx context.Context, store esv1beta1.GenericStore,
 	return c.newClient(ctx, store, kube, clientset.CoreV1(), namespace)
 	return c.newClient(ctx, store, kube, clientset.CoreV1(), namespace)
 }
 }
 
 
-func (c *connector) newClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, corev1 typedcorev1.CoreV1Interface, namespace string) (esv1beta1.SecretsClient, error) {
+func (c *Connector) newClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, corev1 typedcorev1.CoreV1Interface, namespace string) (esv1beta1.SecretsClient, error) {
 	storeSpec := store.GetSpec()
 	storeSpec := store.GetSpec()
 	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Vault == nil {
 	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Vault == nil {
 		return nil, errors.New(errVaultStore)
 		return nil, errors.New(errVaultStore)
 	}
 	}
 	vaultSpec := storeSpec.Provider.Vault
 	vaultSpec := storeSpec.Provider.Vault
 
 
+	vStore, cfg, err := c.prepareConfig(kube, corev1, vaultSpec, namespace, store.GetObjectKind().GroupVersionKind().Kind)
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := getVaultClient(c, store, cfg)
+	if err != nil {
+		return nil, fmt.Errorf(errVaultClient, err)
+	}
+
+	return c.initClient(ctx, vStore, client, cfg, vaultSpec)
+}
+
+func (c *Connector) NewGeneratorClient(ctx context.Context, kube kclient.Client, corev1 typedcorev1.CoreV1Interface, vaultSpec *esv1beta1.VaultProvider, namespace string) (util.Client, error) {
+	vStore, cfg, err := c.prepareConfig(kube, corev1, vaultSpec, namespace, "Generator")
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := c.NewVaultClient(cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = c.initClient(ctx, vStore, client, cfg, vaultSpec)
+	if err != nil {
+		return nil, err
+	}
+
+	return client, nil
+}
+
+func (c *Connector) prepareConfig(kube kclient.Client, corev1 typedcorev1.CoreV1Interface, vaultSpec *esv1beta1.VaultProvider, namespace, storeKind string) (*client, *vault.Config, error) {
 	vStore := &client{
 	vStore := &client{
 		kube:      kube,
 		kube:      kube,
 		corev1:    corev1,
 		corev1:    corev1,
 		store:     vaultSpec,
 		store:     vaultSpec,
 		log:       logger,
 		log:       logger,
 		namespace: namespace,
 		namespace: namespace,
-		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
+		storeKind: storeKind,
 	}
 	}
 
 
 	cfg, err := vStore.newConfig()
 	cfg, err := vStore.newConfig()
 	if err != nil {
 	if err != nil {
-		return nil, err
-	}
-
-	client, err := getVaultClient(c, store, cfg)
-	if err != nil {
-		return nil, fmt.Errorf(errVaultClient, err)
+		return nil, nil, err
 	}
 	}
+	return vStore, cfg, nil
+}
 
 
+func (c *Connector) initClient(ctx context.Context, vStore *client, client util.Client, cfg *vault.Config, vaultSpec *esv1beta1.VaultProvider) (esv1beta1.SecretsClient, error) {
 	if vaultSpec.Namespace != nil {
 	if vaultSpec.Namespace != nil {
 		client.SetNamespace(*vaultSpec.Namespace)
 		client.SetNamespace(*vaultSpec.Namespace)
 	}
 	}
@@ -327,7 +289,7 @@ func (c *connector) newClient(ctx context.Context, store esv1beta1.GenericStore,
 	return vStore, nil
 	return vStore, nil
 }
 }
 
 
-func (c *connector) ValidateStore(store esv1beta1.GenericStore) error {
+func (c *Connector) ValidateStore(store esv1beta1.GenericStore) error {
 	if store == nil {
 	if store == nil {
 		return fmt.Errorf(errInvalidStore)
 		return fmt.Errorf(errInvalidStore)
 	}
 	}
@@ -669,7 +631,7 @@ func (v *client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
 	// actual keys to take precedence over gjson syntax
 	// actual keys to take precedence over gjson syntax
 	// (2): extract key from secret with property
 	// (2): extract key from secret with property
 	if _, ok := data[ref.Property]; ok {
 	if _, ok := data[ref.Property]; ok {
-		return getTypedKey(data, ref.Property)
+		return GetTypedKey(data, ref.Property)
 	}
 	}
 
 
 	// (3): extract key from secret using gjson
 	// (3): extract key from secret using gjson
@@ -696,7 +658,7 @@ func (v *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretD
 	}
 	}
 	byteMap := make(map[string][]byte, len(secretData))
 	byteMap := make(map[string][]byte, len(secretData))
 	for k := range secretData {
 	for k := range secretData {
-		byteMap[k], err = getTypedKey(secretData, k)
+		byteMap[k], err = GetTypedKey(secretData, k)
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
@@ -705,7 +667,7 @@ func (v *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretD
 	return byteMap, nil
 	return byteMap, nil
 }
 }
 
 
-func getTypedKey(data map[string]interface{}, key string) ([]byte, error) {
+func GetTypedKey(data map[string]interface{}, key string) ([]byte, error) {
 	v, ok := data[key]
 	v, ok := data[key]
 	if !ok {
 	if !ok {
 		return nil, fmt.Errorf(errUnexpectedKey, key)
 		return nil, fmt.Errorf(errUnexpectedKey, key)
@@ -715,17 +677,23 @@ func getTypedKey(data map[string]interface{}, key string) ([]byte, error) {
 		return []byte(t), nil
 		return []byte(t), nil
 	case map[string]interface{}:
 	case map[string]interface{}:
 		return json.Marshal(t)
 		return json.Marshal(t)
+	case []string:
+		return []byte(strings.Join(t, "\n")), nil
 	case []byte:
 	case []byte:
 		return t, nil
 		return t, nil
 	// also covers int and float32 due to json.Marshal
 	// also covers int and float32 due to json.Marshal
 	case float64:
 	case float64:
 		return []byte(strconv.FormatFloat(t, 'f', -1, 64)), nil
 		return []byte(strconv.FormatFloat(t, 'f', -1, 64)), nil
+	case json.Number:
+		return []byte(t.String()), nil
+	case []interface{}:
+		return json.Marshal(t)
 	case bool:
 	case bool:
 		return []byte(strconv.FormatBool(t)), nil
 		return []byte(strconv.FormatBool(t)), nil
 	case nil:
 	case nil:
 		return []byte(nil), nil
 		return []byte(nil), nil
 	default:
 	default:
-		return nil, errors.New(errSecretFormat)
+		return nil, fmt.Errorf(errSecretFormat, key, reflect.TypeOf(t))
 	}
 	}
 }
 }
 
 
@@ -1220,7 +1188,7 @@ func (v *client) serviceAccountToken(ctx context.Context, serviceAccountRef esme
 }
 }
 
 
 // checkToken does a lookup and checks if the provided token exists.
 // checkToken does a lookup and checks if the provided token exists.
-func checkToken(ctx context.Context, token Token) (bool, error) {
+func checkToken(ctx context.Context, token util.Token) (bool, error) {
 	// https://www.vaultproject.io/api-docs/auth/token#lookup-a-token-self
 	// https://www.vaultproject.io/api-docs/auth/token#lookup-a-token-self
 	resp, err := token.LookupSelfWithContext(ctx)
 	resp, err := token.LookupSelfWithContext(ctx)
 	metrics.ObserveAPICall(metrics.ProviderHCVault, metrics.CallHCVaultLookupSelf, err)
 	metrics.ObserveAPICall(metrics.ProviderHCVault, metrics.CallHCVaultLookupSelf, err)
@@ -1238,7 +1206,7 @@ func checkToken(ctx context.Context, token Token) (bool, error) {
 	return true, nil
 	return true, nil
 }
 }
 
 
-func revokeTokenIfValid(ctx context.Context, client Client) error {
+func revokeTokenIfValid(ctx context.Context, client util.Client) error {
 	valid, err := checkToken(ctx, client.AuthToken())
 	valid, err := checkToken(ctx, client.AuthToken())
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf(errVaultRevokeToken, err)
 		return fmt.Errorf(errVaultRevokeToken, err)
@@ -1441,7 +1409,7 @@ func init() {
 	fs.IntVar(&vaultTokenCacheSize, "experimental-vault-token-cache-size", 2<<17, "Maximum size of Vault token cache. When more tokens than Only used if --experimental-enable-vault-token-cache is set.")
 	fs.IntVar(&vaultTokenCacheSize, "experimental-vault-token-cache-size", 2<<17, "Maximum size of Vault token cache. When more tokens than Only used if --experimental-enable-vault-token-cache is set.")
 	lateInit := func() {
 	lateInit := func() {
 		logger.Info("initializing vault cache with size=%d", vaultTokenCacheSize)
 		logger.Info("initializing vault cache with size=%d", vaultTokenCacheSize)
-		clientCache = cache.Must(vaultTokenCacheSize, func(client Client) {
+		clientCache = cache.Must(vaultTokenCacheSize, func(client util.Client) {
 			err := revokeTokenIfValid(context.Background(), client)
 			err := revokeTokenIfValid(context.Background(), client)
 			if err != nil {
 			if err != nil {
 				logger.Error(err, "unable to revoke cached token on eviction")
 				logger.Error(err, "unable to revoke cached token on eviction")
@@ -1453,8 +1421,8 @@ func init() {
 		Initialize: lateInit,
 		Initialize: lateInit,
 	})
 	})
 
 
-	esv1beta1.Register(&connector{
-		newVaultClient: newVaultClient,
+	esv1beta1.Register(&Connector{
+		NewVaultClient: NewVaultClient,
 	}, &esv1beta1.SecretStoreProvider{
 	}, &esv1beta1.SecretStoreProvider{
 		Vault: &esv1beta1.VaultProvider{},
 		Vault: &esv1beta1.VaultProvider{},
 	})
 	})

+ 59 - 43
pkg/provider/vault/vault_test.go

@@ -16,6 +16,7 @@ package vault
 
 
 import (
 import (
 	"context"
 	"context"
+	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"reflect"
 	"reflect"
@@ -35,6 +36,7 @@ import (
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 	utilfake "github.com/external-secrets/external-secrets/pkg/provider/util/fake"
 	utilfake "github.com/external-secrets/external-secrets/pkg/provider/util/fake"
 	"github.com/external-secrets/external-secrets/pkg/provider/vault/fake"
 	"github.com/external-secrets/external-secrets/pkg/provider/vault/fake"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/util"
 )
 )
 
 
 const (
 const (
@@ -172,7 +174,7 @@ func makeSecretStore(tweaks ...secretStoreTweakFn) *esv1beta1.SecretStore {
 }
 }
 
 
 type args struct {
 type args struct {
-	newClientFunc func(c *vault.Config) (Client, error)
+	newClientFunc func(c *vault.Config) (util.Client, error)
 	store         esv1beta1.GenericStore
 	store         esv1beta1.GenericStore
 	kube          kclient.Client
 	kube          kclient.Client
 	corev1        typedcorev1.CoreV1Interface
 	corev1        typedcorev1.CoreV1Interface
@@ -189,30 +191,6 @@ type testCase struct {
 	want   want
 	want   want
 }
 }
 
 
-func clientWithLoginMock(c *vault.Config) (Client, error) {
-	cl := fake.VaultClient{
-		MockAuthToken: fake.NewAuthTokenFn(),
-		MockSetToken:  fake.NewSetTokenFn(),
-		MockToken:     fake.NewTokenFn(""),
-		MockAuth:      fake.NewVaultAuth(),
-		MockLogical:   fake.NewVaultLogical(),
-	}
-	auth := cl.Auth()
-	token := cl.AuthToken()
-	logical := cl.Logical()
-	out := VClient{
-		setToken:     cl.SetToken,
-		token:        cl.Token,
-		clearToken:   cl.ClearToken,
-		auth:         auth,
-		authToken:    token,
-		logical:      logical,
-		setNamespace: cl.SetNamespace,
-		addHeader:    cl.AddHeader,
-	}
-	return out, nil
-}
-
 func TestNewVault(t *testing.T) {
 func TestNewVault(t *testing.T) {
 	errBoom := errors.New("boom")
 	errBoom := errors.New("boom")
 	secretClientKey := []byte(`-----BEGIN PRIVATE KEY-----
 	secretClientKey := []byte(`-----BEGIN PRIVATE KEY-----
@@ -258,7 +236,7 @@ MIIFkTCCA3mgAwIBAgIUBEUg3m/WqAsWHG4Q/II3IePFfuowDQYJKoZIhvcNAQELBQAwWDELMAkGA1UE
 		"GetKubeServiceAccountError": {
 		"GetKubeServiceAccountError": {
 			reason: "Should return error if fetching kubernetes secret fails.",
 			reason: "Should return error if fetching kubernetes secret fails.",
 			args: args{
 			args: args{
-				newClientFunc: clientWithLoginMock,
+				newClientFunc: fake.ClientWithLoginMock,
 				ns:            "default",
 				ns:            "default",
 				kube:          clientfake.NewClientBuilder().Build(),
 				kube:          clientfake.NewClientBuilder().Build(),
 				store:         makeSecretStore(),
 				store:         makeSecretStore(),
@@ -300,14 +278,14 @@ MIIFkTCCA3mgAwIBAgIUBEUg3m/WqAsWHG4Q/II3IePFfuowDQYJKoZIhvcNAQELBQAwWDELMAkGA1UE
 						"tls.crt": clientCrt,
 						"tls.crt": clientCrt,
 					},
 					},
 				}).Build(),
 				}).Build(),
-				newClientFunc: clientWithLoginMock,
+				newClientFunc: fake.ClientWithLoginMock,
 			},
 			},
 			want: want{
 			want: want{
 				err: nil,
 				err: nil,
 			},
 			},
 		},
 		},
 		"SuccessfulVaultStoreWithK8sCertSecret": {
 		"SuccessfulVaultStoreWithK8sCertSecret": {
-			reason: "Should return a Vault prodvider with the cert from k8s",
+			reason: "Should return a Vault provider with the cert from k8s",
 			args: args{
 			args: args{
 				store: makeValidSecretStoreWithK8sCerts(true),
 				store: makeValidSecretStoreWithK8sCerts(true),
 				ns:    "default",
 				ns:    "default",
@@ -322,7 +300,7 @@ MIIFkTCCA3mgAwIBAgIUBEUg3m/WqAsWHG4Q/II3IePFfuowDQYJKoZIhvcNAQELBQAwWDELMAkGA1UE
 					},
 					},
 				}).Build(),
 				}).Build(),
 				corev1:        utilfake.NewCreateTokenMock().WithToken("ok"),
 				corev1:        utilfake.NewCreateTokenMock().WithToken("ok"),
-				newClientFunc: clientWithLoginMock,
+				newClientFunc: fake.ClientWithLoginMock,
 			},
 			},
 			want: want{
 			want: want{
 				err: nil,
 				err: nil,
@@ -351,7 +329,7 @@ MIIFkTCCA3mgAwIBAgIUBEUg3m/WqAsWHG4Q/II3IePFfuowDQYJKoZIhvcNAQELBQAwWDELMAkGA1UE
 					},
 					},
 					Data: map[string][]byte{},
 					Data: map[string][]byte{},
 				}).Build(),
 				}).Build(),
-				newClientFunc: clientWithLoginMock,
+				newClientFunc: fake.ClientWithLoginMock,
 			},
 			},
 			want: want{
 			want: want{
 				err: fmt.Errorf(errVaultCert, errors.New(`cannot find secret data for key: "cert"`)),
 				err: fmt.Errorf(errVaultCert, errors.New(`cannot find secret data for key: "cert"`)),
@@ -371,7 +349,7 @@ MIIFkTCCA3mgAwIBAgIUBEUg3m/WqAsWHG4Q/II3IePFfuowDQYJKoZIhvcNAQELBQAwWDELMAkGA1UE
 					},
 					},
 				}).Build(),
 				}).Build(),
 				corev1:        utilfake.NewCreateTokenMock().WithToken("ok"),
 				corev1:        utilfake.NewCreateTokenMock().WithToken("ok"),
-				newClientFunc: clientWithLoginMock,
+				newClientFunc: fake.ClientWithLoginMock,
 			},
 			},
 			want: want{
 			want: want{
 				err: nil,
 				err: nil,
@@ -398,7 +376,7 @@ MIIFkTCCA3mgAwIBAgIUBEUg3m/WqAsWHG4Q/II3IePFfuowDQYJKoZIhvcNAQELBQAwWDELMAkGA1UE
 					},
 					},
 					Data: map[string]string{},
 					Data: map[string]string{},
 				}).Build(),
 				}).Build(),
-				newClientFunc: clientWithLoginMock,
+				newClientFunc: fake.ClientWithLoginMock,
 			},
 			},
 			want: want{
 			want: want{
 				err: fmt.Errorf(errConfigMapFmt, "cert"),
 				err: fmt.Errorf(errConfigMapFmt, "cert"),
@@ -419,7 +397,7 @@ MIIFkTCCA3mgAwIBAgIUBEUg3m/WqAsWHG4Q/II3IePFfuowDQYJKoZIhvcNAQELBQAwWDELMAkGA1UE
 						"tls.crt": []byte("cert with mistak"),
 						"tls.crt": []byte("cert with mistak"),
 					},
 					},
 				}).Build(),
 				}).Build(),
-				newClientFunc: clientWithLoginMock,
+				newClientFunc: fake.ClientWithLoginMock,
 			},
 			},
 			want: want{
 			want: want{
 				err: fmt.Errorf(errClientTLSAuth, "tls: failed to find any PEM data in certificate input"),
 				err: fmt.Errorf(errClientTLSAuth, "tls: failed to find any PEM data in certificate input"),
@@ -440,7 +418,7 @@ MIIFkTCCA3mgAwIBAgIUBEUg3m/WqAsWHG4Q/II3IePFfuowDQYJKoZIhvcNAQELBQAwWDELMAkGA1UE
 						"tls.crt": clientCrt,
 						"tls.crt": clientCrt,
 					},
 					},
 				}).Build(),
 				}).Build(),
-				newClientFunc: clientWithLoginMock,
+				newClientFunc: fake.ClientWithLoginMock,
 			},
 			},
 			want: want{
 			want: want{
 				err: fmt.Errorf(errClientTLSAuth, "tls: failed to find any PEM data in key input"),
 				err: fmt.Errorf(errClientTLSAuth, "tls: failed to find any PEM data in key input"),
@@ -456,11 +434,11 @@ MIIFkTCCA3mgAwIBAgIUBEUg3m/WqAsWHG4Q/II3IePFfuowDQYJKoZIhvcNAQELBQAwWDELMAkGA1UE
 }
 }
 
 
 func vaultTest(t *testing.T, name string, tc testCase) {
 func vaultTest(t *testing.T, name string, tc testCase) {
-	conn := &connector{
-		newVaultClient: tc.args.newClientFunc,
+	conn := &Connector{
+		NewVaultClient: tc.args.newClientFunc,
 	}
 	}
 	if tc.args.newClientFunc == nil {
 	if tc.args.newClientFunc == nil {
-		conn.newVaultClient = newVaultClient
+		conn.NewVaultClient = NewVaultClient
 	}
 	}
 	_, err := conn.newClient(context.Background(), tc.args.store, tc.args.kube, tc.args.corev1, tc.args.ns)
 	_, err := conn.newClient(context.Background(), tc.args.store, tc.args.kube, tc.args.corev1, tc.args.ns)
 	if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
 	if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
@@ -487,12 +465,18 @@ func TestGetSecret(t *testing.T) {
 			"foo": "oke",
 			"foo": "oke",
 			"bar": "also ok?",
 			"bar": "also ok?",
 		},
 		},
+		"list_of_values": []string{
+			"first_value",
+			"second_value",
+			"third_value",
+		},
+		"json_number": json.Number("42"),
 	}
 	}
 
 
 	type args struct {
 	type args struct {
 		store    *esv1beta1.VaultProvider
 		store    *esv1beta1.VaultProvider
 		kube     kclient.Client
 		kube     kclient.Client
-		vLogical Logical
+		vLogical util.Logical
 		ns       string
 		ns       string
 		data     esv1beta1.ExternalSecretDataRemoteRef
 		data     esv1beta1.ExternalSecretDataRemoteRef
 	}
 	}
@@ -586,6 +570,38 @@ func TestGetSecret(t *testing.T) {
 				val: []byte("something different"),
 				val: []byte("something different"),
 			},
 			},
 		},
 		},
+		"ReadSecretWithSliceValue": {
+			reason: "Should return property as a joined slice",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					Property: "list_of_values",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretWithNestedVal, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: []byte("first_value\nsecond_value\nthird_value"),
+			},
+		},
+		"ReadSecretWithJsonNumber": {
+			reason: "Should return parsed json.Number property",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					Property: "json_number",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretWithNestedVal, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: []byte("42"),
+			},
+		},
 		"NonexistentProperty": {
 		"NonexistentProperty": {
 			reason: "Should return error property does not exist.",
 			reason: "Should return error property does not exist.",
 			args: args{
 			args: args{
@@ -763,7 +779,7 @@ func TestGetSecretMap(t *testing.T) {
 	type args struct {
 	type args struct {
 		store   *esv1beta1.VaultProvider
 		store   *esv1beta1.VaultProvider
 		kube    kclient.Client
 		kube    kclient.Client
-		vClient Logical
+		vClient util.Logical
 		ns      string
 		ns      string
 		data    esv1beta1.ExternalSecretDataRemoteRef
 		data    esv1beta1.ExternalSecretDataRemoteRef
 	}
 	}
@@ -1075,7 +1091,7 @@ func TestGetAllSecrets(t *testing.T) {
 	type args struct {
 	type args struct {
 		store    *esv1beta1.VaultProvider
 		store    *esv1beta1.VaultProvider
 		kube     kclient.Client
 		kube     kclient.Client
-		vLogical Logical
+		vLogical util.Logical
 		ns       string
 		ns       string
 		data     esv1beta1.ExternalSecretFind
 		data     esv1beta1.ExternalSecretFind
 	}
 	}
@@ -1436,8 +1452,8 @@ func TestValidateStore(t *testing.T) {
 	}
 	}
 	for _, tt := range tests {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
-			c := &connector{
-				newVaultClient: nil,
+			c := &Connector{
+				NewVaultClient: nil,
 			}
 			}
 			store := &esv1beta1.SecretStore{
 			store := &esv1beta1.SecretStore{
 				Spec: esv1beta1.SecretStoreSpec{
 				Spec: esv1beta1.SecretStoreSpec{
@@ -1468,7 +1484,7 @@ func TestSetSecret(t *testing.T) {
 
 
 	type args struct {
 	type args struct {
 		store    *esv1beta1.VaultProvider
 		store    *esv1beta1.VaultProvider
-		vLogical Logical
+		vLogical util.Logical
 	}
 	}
 
 
 	type want struct {
 	type want struct {

+ 1 - 2
pkg/utils/utils.go

@@ -15,8 +15,7 @@ limitations under the License.
 package utils
 package utils
 
 
 import (
 import (
-	//nolint:gosec
-	"crypto/md5"
+	"crypto/md5" //nolint:gosec
 	"encoding/base64"
 	"encoding/base64"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"