Browse Source

✨Implements dataFrom key rewrite (#1381)

* Implements dataFrom key rewrite

Co-authored-by: Moritz Johner <moolen@users.noreply.github.com>
Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>

* docs: add example to remove invalid characters

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>
Gustavo Fernandes de Carvalho 3 years ago
parent
commit
b4e7acfaa9
30 changed files with 949 additions and 33 deletions
  1. 21 4
      apis/externalsecrets/v1beta1/externalsecret_types.go
  2. 42 0
      apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
  3. 29 5
      config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml
  4. 28 5
      config/crds/bases/external-secrets.io_externalsecrets.yaml
  5. 44 10
      deploy/crds/bundle.yaml
  6. 105 0
      docs/guides-datafrom-rewrite.md
  7. 7 2
      docs/guides-getallsecrets.md
  8. 24 0
      docs/snippets/datafrom-rewrite-conflict.yaml
  9. 18 0
      docs/snippets/datafrom-rewrite-invalid-characters.yaml
  10. 20 0
      docs/snippets/datafrom-rewrite-remove-path.yaml
  11. 14 0
      docs/snippets/full-external-secret.yaml
  12. 86 0
      docs/spec.md
  13. 1 0
      e2e/suites/provider/cases/akeyless/akeyless.go
  14. 1 0
      e2e/suites/provider/cases/alibaba/alibaba.go
  15. 1 0
      e2e/suites/provider/cases/aws/parameterstore/parameterstore.go
  16. 1 0
      e2e/suites/provider/cases/aws/secretsmanager/secretsmanager.go
  17. 1 0
      e2e/suites/provider/cases/azure/azure_secret.go
  18. 37 0
      e2e/suites/provider/cases/common/common.go
  19. 50 2
      e2e/suites/provider/cases/common/find_by_name.go
  20. 2 0
      e2e/suites/provider/cases/gcp/gcp.go
  21. 1 0
      e2e/suites/provider/cases/gitlab/gitlab.go
  22. 1 0
      e2e/suites/provider/cases/kubernetes/kubernetes.go
  23. 1 0
      e2e/suites/provider/cases/oracle/oracle.go
  24. 12 0
      e2e/suites/provider/cases/vault/vault.go
  25. 1 0
      hack/api-docs/mkdocs.yml
  26. 26 5
      pkg/controllers/externalsecret/externalsecret_controller.go
  27. 168 0
      pkg/controllers/externalsecret/externalsecret_controller_test.go
  28. 3 0
      pkg/provider/vault/vault.go
  29. 46 0
      pkg/utils/utils.go
  30. 158 0
      pkg/utils/utils_test.go

+ 21 - 4
apis/externalsecrets/v1beta1/externalsecret_types.go

@@ -184,7 +184,7 @@ type ExternalSecretDataRemoteRef struct {
 	ConversionStrategy ExternalSecretConversionStrategy `json:"conversionStrategy,omitempty"`
 	ConversionStrategy ExternalSecretConversionStrategy `json:"conversionStrategy,omitempty"`
 
 
 	// +optional
 	// +optional
-	// Used to define a conversion Strategy
+	// Used to define a decoding Strategy
 	// +kubebuilder:default="None"
 	// +kubebuilder:default="None"
 	DecodingStrategy ExternalSecretDecodingStrategy `json:"decodingStrategy,omitempty"`
 	DecodingStrategy ExternalSecretDecodingStrategy `json:"decodingStrategy,omitempty"`
 }
 }
@@ -212,8 +212,6 @@ const (
 	ExternalSecretDecodeNone      ExternalSecretDecodingStrategy = "None"
 	ExternalSecretDecodeNone      ExternalSecretDecodingStrategy = "None"
 )
 )
 
 
-// +kubebuilder:validation:MinProperties=1
-// +kubebuilder:validation:MaxProperties=1
 type ExternalSecretDataFromRemoteRef struct {
 type ExternalSecretDataFromRemoteRef struct {
 	// Used to extract multiple key/value pairs from one secret
 	// Used to extract multiple key/value pairs from one secret
 	// +optional
 	// +optional
@@ -221,8 +219,26 @@ type ExternalSecretDataFromRemoteRef struct {
 	// Used to find secrets based on tags or regular expressions
 	// Used to find secrets based on tags or regular expressions
 	// +optional
 	// +optional
 	Find *ExternalSecretFind `json:"find,omitempty"`
 	Find *ExternalSecretFind `json:"find,omitempty"`
+
+	// Used to rewrite secret Keys after getting them from the secret Provider
+	// Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
+	// +optional
+	Rewrite []ExternalSecretRewrite `json:"rewrite,omitempty"`
 }
 }
 
 
+type ExternalSecretRewrite struct {
+	// Used to rewrite with regular expressions.
+	// The resulting key will be the output of a regexp.ReplaceAll operation.
+	// +optional
+	Regexp *ExternalSecretRewriteRegexp `json:"regexp,omitempty"`
+}
+
+type ExternalSecretRewriteRegexp struct {
+	// Used to define the regular expression of a re.Compiler.
+	Source string `json:"source"`
+	// Used to define the target pattern of a ReplaceAll operation.
+	Target string `json:"target"`
+}
 type ExternalSecretFind struct {
 type ExternalSecretFind struct {
 	// A root path to start the find operations.
 	// A root path to start the find operations.
 	// +optional
 	// +optional
@@ -241,7 +257,7 @@ type ExternalSecretFind struct {
 	ConversionStrategy ExternalSecretConversionStrategy `json:"conversionStrategy,omitempty"`
 	ConversionStrategy ExternalSecretConversionStrategy `json:"conversionStrategy,omitempty"`
 
 
 	// +optional
 	// +optional
-	// Used to define a conversion Strategy
+	// Used to define a decoding Strategy
 	// +kubebuilder:default="None"
 	// +kubebuilder:default="None"
 	DecodingStrategy ExternalSecretDecodingStrategy `json:"decodingStrategy,omitempty"`
 	DecodingStrategy ExternalSecretDecodingStrategy `json:"decodingStrategy,omitempty"`
 }
 }
@@ -307,6 +323,7 @@ const (
 	ReasonUnavailableStore     = "UnavailableStore"
 	ReasonUnavailableStore     = "UnavailableStore"
 	ReasonProviderClientConfig = "InvalidProviderClientConfig"
 	ReasonProviderClientConfig = "InvalidProviderClientConfig"
 	ReasonUpdateFailed         = "UpdateFailed"
 	ReasonUpdateFailed         = "UpdateFailed"
+	ReasonDeprecated           = "ParameterDeprecated"
 	ReasonUpdated              = "Updated"
 	ReasonUpdated              = "Updated"
 	ReasonDeleted              = "Deleted"
 	ReasonDeleted              = "Deleted"
 )
 )

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

@@ -578,6 +578,13 @@ func (in *ExternalSecretDataFromRemoteRef) DeepCopyInto(out *ExternalSecretDataF
 		*out = new(ExternalSecretFind)
 		*out = new(ExternalSecretFind)
 		(*in).DeepCopyInto(*out)
 		(*in).DeepCopyInto(*out)
 	}
 	}
+	if in.Rewrite != nil {
+		in, out := &in.Rewrite, &out.Rewrite
+		*out = make([]ExternalSecretRewrite, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 }
 
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretDataFromRemoteRef.
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretDataFromRemoteRef.
@@ -670,6 +677,41 @@ func (in *ExternalSecretList) DeepCopyObject() runtime.Object {
 }
 }
 
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExternalSecretRewrite) DeepCopyInto(out *ExternalSecretRewrite) {
+	*out = *in
+	if in.Regexp != nil {
+		in, out := &in.Regexp, &out.Regexp
+		*out = new(ExternalSecretRewriteRegexp)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretRewrite.
+func (in *ExternalSecretRewrite) DeepCopy() *ExternalSecretRewrite {
+	if in == nil {
+		return nil
+	}
+	out := new(ExternalSecretRewrite)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExternalSecretRewriteRegexp) DeepCopyInto(out *ExternalSecretRewriteRegexp) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretRewriteRegexp.
+func (in *ExternalSecretRewriteRegexp) DeepCopy() *ExternalSecretRewriteRegexp {
+	if in == nil {
+		return nil
+	}
+	out := new(ExternalSecretRewriteRegexp)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ExternalSecretSpec) DeepCopyInto(out *ExternalSecretSpec) {
 func (in *ExternalSecretSpec) DeepCopyInto(out *ExternalSecretSpec) {
 	*out = *in
 	*out = *in
 	out.SecretStoreRef = in.SecretStoreRef
 	out.SecretStoreRef = in.SecretStoreRef

+ 29 - 5
config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml

@@ -77,7 +77,7 @@ spec:
                               type: string
                               type: string
                             decodingStrategy:
                             decodingStrategy:
                               default: None
                               default: None
-                              description: Used to define a conversion Strategy
+                              description: Used to define a decoding Strategy
                               type: string
                               type: string
                             key:
                             key:
                               description: Key is the key used in the Provider, mandatory
                               description: Key is the key used in the Provider, mandatory
@@ -110,8 +110,6 @@ spec:
                       Provider data If multiple entries are specified, the Secret
                       Provider data If multiple entries are specified, the Secret
                       keys are merged in the specified order
                       keys are merged in the specified order
                     items:
                     items:
-                      maxProperties: 1
-                      minProperties: 1
                       properties:
                       properties:
                         extract:
                         extract:
                           description: Used to extract multiple key/value pairs from
                           description: Used to extract multiple key/value pairs from
@@ -123,7 +121,7 @@ spec:
                               type: string
                               type: string
                             decodingStrategy:
                             decodingStrategy:
                               default: None
                               default: None
-                              description: Used to define a conversion Strategy
+                              description: Used to define a decoding Strategy
                               type: string
                               type: string
                             key:
                             key:
                               description: Key is the key used in the Provider, mandatory
                               description: Key is the key used in the Provider, mandatory
@@ -154,7 +152,7 @@ spec:
                               type: string
                               type: string
                             decodingStrategy:
                             decodingStrategy:
                               default: None
                               default: None
-                              description: Used to define a conversion Strategy
+                              description: Used to define a decoding Strategy
                               type: string
                               type: string
                             name:
                             name:
                               description: Finds secrets based on the name.
                               description: Finds secrets based on the name.
@@ -172,6 +170,32 @@ spec:
                               description: Find secrets based on tags.
                               description: Find secrets based on tags.
                               type: object
                               type: object
                           type: object
                           type: object
+                        rewrite:
+                          description: Used to rewrite secret Keys after getting them
+                            from the secret Provider Multiple Rewrite operations can
+                            be provided. They are applied in a layered order (first
+                            to last)
+                          items:
+                            properties:
+                              regexp:
+                                description: Used to rewrite with regular expressions.
+                                  The resulting key will be the output of a regexp.ReplaceAll
+                                  operation.
+                                properties:
+                                  source:
+                                    description: Used to define the regular expression
+                                      of a re.Compiler.
+                                    type: string
+                                  target:
+                                    description: Used to define the target pattern
+                                      of a ReplaceAll operation.
+                                    type: string
+                                required:
+                                - source
+                                - target
+                                type: object
+                            type: object
+                          type: array
                       type: object
                       type: object
                     type: array
                     type: array
                   refreshInterval:
                   refreshInterval:

+ 28 - 5
config/crds/bases/external-secrets.io_externalsecrets.yaml

@@ -312,7 +312,7 @@ spec:
                           type: string
                           type: string
                         decodingStrategy:
                         decodingStrategy:
                           default: None
                           default: None
-                          description: Used to define a conversion Strategy
+                          description: Used to define a decoding Strategy
                           type: string
                           type: string
                         key:
                         key:
                           description: Key is the key used in the Provider, mandatory
                           description: Key is the key used in the Provider, mandatory
@@ -345,8 +345,6 @@ spec:
                   Provider data If multiple entries are specified, the Secret keys
                   Provider data If multiple entries are specified, the Secret keys
                   are merged in the specified order
                   are merged in the specified order
                 items:
                 items:
-                  maxProperties: 1
-                  minProperties: 1
                   properties:
                   properties:
                     extract:
                     extract:
                       description: Used to extract multiple key/value pairs from one
                       description: Used to extract multiple key/value pairs from one
@@ -358,7 +356,7 @@ spec:
                           type: string
                           type: string
                         decodingStrategy:
                         decodingStrategy:
                           default: None
                           default: None
-                          description: Used to define a conversion Strategy
+                          description: Used to define a decoding Strategy
                           type: string
                           type: string
                         key:
                         key:
                           description: Key is the key used in the Provider, mandatory
                           description: Key is the key used in the Provider, mandatory
@@ -388,7 +386,7 @@ spec:
                           type: string
                           type: string
                         decodingStrategy:
                         decodingStrategy:
                           default: None
                           default: None
-                          description: Used to define a conversion Strategy
+                          description: Used to define a decoding Strategy
                           type: string
                           type: string
                         name:
                         name:
                           description: Finds secrets based on the name.
                           description: Finds secrets based on the name.
@@ -406,6 +404,31 @@ spec:
                           description: Find secrets based on tags.
                           description: Find secrets based on tags.
                           type: object
                           type: object
                       type: object
                       type: object
+                    rewrite:
+                      description: Used to rewrite secret Keys after getting them
+                        from the secret Provider Multiple Rewrite operations can be
+                        provided. They are applied in a layered order (first to last)
+                      items:
+                        properties:
+                          regexp:
+                            description: Used to rewrite with regular expressions.
+                              The resulting key will be the output of a regexp.ReplaceAll
+                              operation.
+                            properties:
+                              source:
+                                description: Used to define the regular expression
+                                  of a re.Compiler.
+                                type: string
+                              target:
+                                description: Used to define the target pattern of
+                                  a ReplaceAll operation.
+                                type: string
+                            required:
+                            - source
+                            - target
+                            type: object
+                        type: object
+                      type: array
                   type: object
                   type: object
                 type: array
                 type: array
               refreshInterval:
               refreshInterval:

+ 44 - 10
deploy/crds/bundle.yaml

@@ -67,7 +67,7 @@ spec:
                                 type: string
                                 type: string
                               decodingStrategy:
                               decodingStrategy:
                                 default: None
                                 default: None
-                                description: Used to define a conversion Strategy
+                                description: Used to define a decoding Strategy
                                 type: string
                                 type: string
                               key:
                               key:
                                 description: Key is the key used in the Provider, mandatory
                                 description: Key is the key used in the Provider, mandatory
@@ -94,8 +94,6 @@ spec:
                     dataFrom:
                     dataFrom:
                       description: DataFrom is used to fetch all properties from a specific Provider data If multiple entries are specified, the Secret keys are merged in the specified order
                       description: DataFrom is used to fetch all properties from a specific Provider data If multiple entries are specified, the Secret keys are merged in the specified order
                       items:
                       items:
-                        maxProperties: 1
-                        minProperties: 1
                         properties:
                         properties:
                           extract:
                           extract:
                             description: Used to extract multiple key/value pairs from one secret
                             description: Used to extract multiple key/value pairs from one secret
@@ -106,7 +104,7 @@ spec:
                                 type: string
                                 type: string
                               decodingStrategy:
                               decodingStrategy:
                                 default: None
                                 default: None
-                                description: Used to define a conversion Strategy
+                                description: Used to define a decoding Strategy
                                 type: string
                                 type: string
                               key:
                               key:
                                 description: Key is the key used in the Provider, mandatory
                                 description: Key is the key used in the Provider, mandatory
@@ -132,7 +130,7 @@ spec:
                                 type: string
                                 type: string
                               decodingStrategy:
                               decodingStrategy:
                                 default: None
                                 default: None
-                                description: Used to define a conversion Strategy
+                                description: Used to define a decoding Strategy
                                 type: string
                                 type: string
                               name:
                               name:
                                 description: Finds secrets based on the name.
                                 description: Finds secrets based on the name.
@@ -150,6 +148,25 @@ spec:
                                 description: Find secrets based on tags.
                                 description: Find secrets based on tags.
                                 type: object
                                 type: object
                             type: object
                             type: object
+                          rewrite:
+                            description: Used to rewrite secret Keys after getting them from the secret Provider Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
+                            items:
+                              properties:
+                                regexp:
+                                  description: Used to rewrite with regular expressions. The resulting key will be the output of a regexp.ReplaceAll operation.
+                                  properties:
+                                    source:
+                                      description: Used to define the regular expression of a re.Compiler.
+                                      type: string
+                                    target:
+                                      description: Used to define the target pattern of a ReplaceAll operation.
+                                      type: string
+                                  required:
+                                    - source
+                                    - target
+                                  type: object
+                              type: object
+                            type: array
                         type: object
                         type: object
                       type: array
                       type: array
                     refreshInterval:
                     refreshInterval:
@@ -2813,7 +2830,7 @@ spec:
                             type: string
                             type: string
                           decodingStrategy:
                           decodingStrategy:
                             default: None
                             default: None
-                            description: Used to define a conversion Strategy
+                            description: Used to define a decoding Strategy
                             type: string
                             type: string
                           key:
                           key:
                             description: Key is the key used in the Provider, mandatory
                             description: Key is the key used in the Provider, mandatory
@@ -2840,8 +2857,6 @@ spec:
                 dataFrom:
                 dataFrom:
                   description: DataFrom is used to fetch all properties from a specific Provider data If multiple entries are specified, the Secret keys are merged in the specified order
                   description: DataFrom is used to fetch all properties from a specific Provider data If multiple entries are specified, the Secret keys are merged in the specified order
                   items:
                   items:
-                    maxProperties: 1
-                    minProperties: 1
                     properties:
                     properties:
                       extract:
                       extract:
                         description: Used to extract multiple key/value pairs from one secret
                         description: Used to extract multiple key/value pairs from one secret
@@ -2852,7 +2867,7 @@ spec:
                             type: string
                             type: string
                           decodingStrategy:
                           decodingStrategy:
                             default: None
                             default: None
-                            description: Used to define a conversion Strategy
+                            description: Used to define a decoding Strategy
                             type: string
                             type: string
                           key:
                           key:
                             description: Key is the key used in the Provider, mandatory
                             description: Key is the key used in the Provider, mandatory
@@ -2878,7 +2893,7 @@ spec:
                             type: string
                             type: string
                           decodingStrategy:
                           decodingStrategy:
                             default: None
                             default: None
-                            description: Used to define a conversion Strategy
+                            description: Used to define a decoding Strategy
                             type: string
                             type: string
                           name:
                           name:
                             description: Finds secrets based on the name.
                             description: Finds secrets based on the name.
@@ -2896,6 +2911,25 @@ spec:
                             description: Find secrets based on tags.
                             description: Find secrets based on tags.
                             type: object
                             type: object
                         type: object
                         type: object
+                      rewrite:
+                        description: Used to rewrite secret Keys after getting them from the secret Provider Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
+                        items:
+                          properties:
+                            regexp:
+                              description: Used to rewrite with regular expressions. The resulting key will be the output of a regexp.ReplaceAll operation.
+                              properties:
+                                source:
+                                  description: Used to define the regular expression of a re.Compiler.
+                                  type: string
+                                target:
+                                  description: Used to define the target pattern of a ReplaceAll operation.
+                                  type: string
+                              required:
+                                - source
+                                - target
+                              type: object
+                          type: object
+                        type: array
                     type: object
                     type: object
                   type: array
                   type: array
                 refreshInterval:
                 refreshInterval:

+ 105 - 0
docs/guides-datafrom-rewrite.md

@@ -0,0 +1,105 @@
+# Rewriting Keys in DataFrom
+
+When calling out an ExternalSecret with `dataFrom.extract` or `dataFrom.find`, it is possible that you end up with a kubernetes secret that has conflicts in the key names, or that you simply want to remove a common path from the secret keys.
+
+In order to do so, it is possible to define a set of rewrite operations using `dataFrom.rewrite`. These operations can be stacked, hence allowing complex manipulations of the secret keys.
+
+Rewrite operations are all applied before `ConversionStrategy` is applied.
+
+## Methods
+
+### Regexp
+This method implements rewriting through the use of regular expressions. It needs a `source` and a `target` field. The source field is where the definition of the matching regular expression goes, where the `target` field is where the replacing expression goes.
+
+Some considerations about the impelementation of Regexp Rewrite:
+
+1. The input of a subsequent rewrite operation are the outputs of the previous rewrite.
+2. If a given set of keys do not match any Rewrite operation, there will be no error. Rather, the original keys will be used.
+3. If a `source` is not a compilable `regexp` expression, an error will be produced and the external secret goes into a error state.
+
+## Examples
+### Removing a common path from find operations
+The following ExternalSecret:
+```yaml
+{% include 'datafrom-rewrite-remove-path.yaml' %}
+```
+Will get all the secrets matching `path/to/my/secrets/*` and then rewrite them by removing the common path away.
+
+In this example, if we had the following secrets available in the provider:
+```
+path/to/my/secrets/username
+path/to/my/secrets/password
+```
+the output kubernetes secret would be:
+```yaml
+apiVersion: v1
+kind: Secret
+type: Opaque
+data:
+    username: ...
+    password: ...
+```
+### Avoiding key collisions
+The following ExternalSecret:
+```yaml
+{% include 'datafrom-rewrite-conflict.yaml' %}
+
+```
+Will allow two secrets with the same JSON keys to be imported into a Kubernetes Secret without any conflict.
+In this example, if we had the following secrets available in the provider:
+```json
+{
+    "my-secrets-dev": {
+        "password": "bar",
+     },
+    "my-secrets-prod": {
+        "password": "safebar",
+     }
+}
+```
+the output kubernetes secret would be:
+```yaml
+apiVersion: v1
+kind: Secret
+type: Opaque
+data:
+    dev_password: YmFy #bar
+    prod_password: c2FmZWJhcg== #safebar
+```
+
+### Remove invalid characters
+The following ExternalSecret:
+```yaml
+{% include 'datafrom-rewrite-invalid-characters.yaml' %}
+
+```
+Will remove invalid characters from the secret key.
+In this example, if we had the following secrets available in the provider:
+```json
+{
+    "development": {
+        "foo/bar": "1111",
+        "foo$baz": "2222"
+    }
+}
+```
+the output kubernetes secret would be:
+```yaml
+apiVersion: v1
+kind: Secret
+type: Opaque
+data:
+    foo_bar: MTExMQ== #1111
+    foo_baz: MjIyMg== #2222
+```
+
+## Limitations
+
+Regexp Rewrite is based on golang `regexp`, which in turns implements `RE2` regexp language. There a a series of known limitations to this implementation, such as:
+
+* Lack of ability to do lookaheads or lookbehinds;
+* Lack of negation expressions;
+* Lack of support to conditionl branches;
+* Lack of support to possessive repetitions.
+
+A list of compatibility and known limitations considering other commonly used regexp frameworks (such as PCRE and PERL) are listed [here](https://github.com/google/re2/wiki/Syntax).

+ 7 - 2
docs/guides-getallsecrets.md

@@ -3,7 +3,7 @@
 In some use cases, it might be impractical to bundle all sensitive information into a single secret, or even it is not possible to fully know a given secret name. In such cases, it is possible that an user might need to sync multiple secrets from an external provider into a single Kubernetes Secret. This is possible to be done in external-secrets with the `dataFrom.find` option.
 In some use cases, it might be impractical to bundle all sensitive information into a single secret, or even it is not possible to fully know a given secret name. In such cases, it is possible that an user might need to sync multiple secrets from an external provider into a single Kubernetes Secret. This is possible to be done in external-secrets with the `dataFrom.find` option.
 
 
 !!! note
 !!! note
-    The secret's contents as defined in the provider are going to be stored in the kubernetes secret as a single key. Currently, it is not possible to apply any decoding Strategy during a find operation.
+    The secret's contents as defined in the provider are going to be stored in the kubernetes secret as a single key. Currently, it possible to apply a decoding Strategy during a find operation, but only at the secret level (e.g. if a secret is a JSON with some B64 encoded data within, `decodingStrategy: Auto` would not decode it)
 
 
 
 
 ### Fetching secrets matching a given name pattern
 ### Fetching secrets matching a given name pattern
@@ -33,7 +33,12 @@ Some providers support filtering out a find operation only to a given path, inst
 ### Avoiding name conflicts
 ### Avoiding name conflicts
 By default, kubernetes Secrets accepts only a given range of characters. `Find` operations will automatically replace any not allowed character with a `_`. So if we have a given secret `a_c` and `a/c` would lead to a naming conflict. 
 By default, kubernetes Secrets accepts only a given range of characters. `Find` operations will automatically replace any not allowed character with a `_`. So if we have a given secret `a_c` and `a/c` would lead to a naming conflict. 
 
 
-It is not entirely possible to avoid this behavior, but setting `dataFrom.find.conversionStrategy: Unicode` reduces the collision probability. When using `Unicode`, any invalid character will be replaced by its unicode, in the form of `_UXXXX_`. In this case, the available kubernetes keys would be `a_c` and `a_U2215_c`, hence avoiding most of possible conflicts.
+
+If you happen to have a case where a conflict is happening, you can use the `rewrite` block to apply a regexp on one of the find operations (for more information please refer to [Rewriting Keys from DataFrom](guides-datafrom-rewrite.md)).
+
+You can also set  `dataFrom.find.conversionStrategy: Unicode` to reduce the collistion probability. When using `Unicode`, any invalid character will be replaced by its unicode, in the form of `_UXXXX_`. In this case, the available kubernetes keys would be `a_c` and `a_U2215_c`, hence avoiding most of possible conflicts.
+
+
 
 
 !!! note "PRs welcome"
 !!! note "PRs welcome"
     Some providers might not have the implementation needed for fetching multiple secrets. If that's your case, please feel free to contribute!
     Some providers might not have the implementation needed for fetching multiple secrets. If that's your case, please feel free to contribute!

+ 24 - 0
docs/snippets/datafrom-rewrite-conflict.yaml

@@ -0,0 +1,24 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: example
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    kind: SecretStore
+    name: backend
+  target:
+    name: secret-to-be-created
+  dataFrom:
+  - extract:
+      key: my-secrets-dev
+    rewrite:
+    - regexp:
+        source: "(.*)"
+        target: "dev-$1"      
+  - extract:
+      key: my-secrets-prod
+    rewrite:
+    - regexp:
+        source: "(.*)"
+        target: "prod-$1"

+ 18 - 0
docs/snippets/datafrom-rewrite-invalid-characters.yaml

@@ -0,0 +1,18 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: example
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    kind: SecretStore
+    name: backend
+  target:
+    name: secret-to-be-created
+  dataFrom:
+  - extract:
+      key: development
+    rewrite:
+    - regexp:
+        source: "[^a-zA-Z0-9 -]"
+        target: "_"

+ 20 - 0
docs/snippets/datafrom-rewrite-remove-path.yaml

@@ -0,0 +1,20 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: example
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    kind: SecretStore
+    name: backend
+  target:
+    name: secret-to-be-created
+  dataFrom:
+  - find:
+      path: path/to/my
+      name: 
+        regexp: secrets
+    rewrite:
+    - regexp:
+        source: "path/to/my/secrets/(.*)"
+        target: "$1"

+ 14 - 0
docs/snippets/full-external-secret.yaml

@@ -85,14 +85,28 @@ spec:
       property: provider-key-property
       property: provider-key-property
       conversionStrategy: Default
       conversionStrategy: Default
       decodingStrategy: Auto
       decodingStrategy: Auto
+    rewrite:
+    - regexp:
+        source: "foo"
+        target: "bar"
+    - regexp:
+        source: "exp-(.*?)-ression"
+        target: "rewriting-$1-with-groups"
   - find:
   - find:
       path: path-to-filter
       path: path-to-filter
+          source: "exp-(.*?)-ression"
+          target: "rewriting-$1-with-groups"
       name:
       name:
         regexp: ".*foobar.*"
         regexp: ".*foobar.*"
       tags:
       tags:
         foo: bar
         foo: bar
       conversionStrategy: Unicode
       conversionStrategy: Unicode
       decodingStrategy: Base64
       decodingStrategy: Base64
+    rewrite:
+    - regexp:
+        source: "foo"
+        target: "bar"
+    - regexp:
 
 
 status:
 status:
   # refreshTime is the time and date the external secret was fetched and
   # refreshTime is the time and date the external secret was fetched and

+ 86 - 0
docs/spec.md

@@ -1522,6 +1522,20 @@ ExternalSecretFind
 <p>Used to find secrets based on tags or regular expressions</p>
 <p>Used to find secrets based on tags or regular expressions</p>
 </td>
 </td>
 </tr>
 </tr>
+<tr>
+<td>
+<code>rewrite</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.ExternalSecretRewrite">
+[]ExternalSecretRewrite
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Used to rewrite secret Keys after getting them from the secret Provider</p>
+</td>
+</tr>
 </tbody>
 </tbody>
 </table>
 </table>
 <h3 id="external-secrets.io/v1beta1.ExternalSecretDataRemoteRef">ExternalSecretDataRemoteRef
 <h3 id="external-secrets.io/v1beta1.ExternalSecretDataRemoteRef">ExternalSecretDataRemoteRef
@@ -1787,6 +1801,78 @@ ExternalSecretDecodingStrategy
 <td></td>
 <td></td>
 </tr></tbody>
 </tr></tbody>
 </table>
 </table>
+<h3 id="external-secrets.io/v1beta1.ExternalSecretRewrite">ExternalSecretRewrite
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.ExternalSecretDataFromRemoteRef">ExternalSecretDataFromRemoteRef</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>regexp</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.ExternalSecretRewriteRegexp">
+ExternalSecretRewriteRegexp
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Rewrite using regular expressions</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.ExternalSecretRewriteRegexp">ExternalSecretRewriteRegexp
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.ExternalSecretRewrite">ExternalSecretRewrite</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>source</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Regular expression to use as a re.Compiler.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>target</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Target output for a replace operation.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.ExternalSecretSpec">ExternalSecretSpec
 <h3 id="external-secrets.io/v1beta1.ExternalSecretSpec">ExternalSecretSpec
 </h3>
 </h3>
 <p>
 <p>

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

@@ -31,6 +31,7 @@ var _ = Describe("[akeyless]", Label("akeyless"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataFromSync(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.DockerJSONConfig(f)),
 		Entry(common.DockerJSONConfig(f)),

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

@@ -31,6 +31,7 @@ var _ = Describe("[alibaba]", Label("alibaba"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataFromSync(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.DockerJSONConfig(f)),
 		Entry(common.DockerJSONConfig(f)),

+ 1 - 0
e2e/suites/provider/cases/aws/parameterstore/parameterstore.go

@@ -33,6 +33,7 @@ var _ = Describe("[aws] ", Label("aws", "parameterstore"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataFromSync(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.DockerJSONConfig(f)),
 		Entry(common.DockerJSONConfig(f)),

+ 1 - 0
e2e/suites/provider/cases/aws/secretsmanager/secretsmanager.go

@@ -33,6 +33,7 @@ var _ = Describe("[aws] ", Label("aws", "secretsmanager"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataFromSync(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.DockerJSONConfig(f)),
 		Entry(common.DockerJSONConfig(f)),

+ 1 - 0
e2e/suites/provider/cases/azure/azure_secret.go

@@ -30,6 +30,7 @@ var _ = Describe("[azure]", Label("azure", "keyvault", "secret"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataFromSync(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.DockerJSONConfig(f)),
 		Entry(common.DockerJSONConfig(f)),

+ 37 - 0
e2e/suites/provider/cases/common/common.go

@@ -291,6 +291,43 @@ func JSONDataFromSync(f *framework.Framework) (string, func(*framework.TestCase)
 	}
 	}
 }
 }
 
 
+// This case creates one secret with json values and syncs them using a single .Spec.DataFrom block.
+func JSONDataFromRewrite(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return "[common] should sync and rewrite secrets with dataFrom", func(tc *framework.TestCase) {
+		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
+		targetSecretKey1 := "username"
+		targetSecretValue1 := "myuser.name"
+		targetSecretKey2 := "address"
+		targetSecretValue2 := "happy street"
+		secretValue := fmt.Sprintf("{ %q: %q, %q: %q }", targetSecretKey1, targetSecretValue1, targetSecretKey2, targetSecretValue2)
+		tc.Secrets = map[string]framework.SecretEntry{
+			secretKey1: {Value: secretValue},
+		}
+		tc.ExpectedSecret = &v1.Secret{
+			Type: v1.SecretTypeOpaque,
+			Data: map[string][]byte{
+				"my_username": []byte(targetSecretValue1),
+				"my_address":  []byte(targetSecretValue2),
+			},
+		}
+		tc.ExternalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
+					Key: secretKey1,
+				},
+				Rewrite: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "(.*)",
+							Target: "my_$1",
+						},
+					},
+				},
+			},
+		}
+	}
+}
+
 // This case creates a secret with a nested json value. It is synced into two secrets.
 // This case creates a secret with a nested json value. It is synced into two secrets.
 // The values from the nested data are extracted using gjson.
 // The values from the nested data are extracted using gjson.
 // not supported by: vault.
 // not supported by: vault.

+ 50 - 2
e2e/suites/provider/cases/common/find_by_name.go

@@ -21,6 +21,10 @@ import (
 	"github.com/external-secrets/external-secrets/e2e/framework"
 	"github.com/external-secrets/external-secrets/e2e/framework"
 )
 )
 
 
+const (
+	findValue = "{\"foo1\":\"foo1-val\"}"
+)
+
 // This case creates multiple secrets with simple key/value pairs and syncs them using multiple .Spec.Data blocks.
 // This case creates multiple secrets with simple key/value pairs and syncs them using multiple .Spec.Data blocks.
 func FindByName(f *framework.Framework) (string, func(*framework.TestCase)) {
 func FindByName(f *framework.Framework) (string, func(*framework.TestCase)) {
 	return "[common] should find secrets by name using .DataFrom[]", func(tc *framework.TestCase) {
 	return "[common] should find secrets by name using .DataFrom[]", func(tc *framework.TestCase) {
@@ -28,7 +32,7 @@ func FindByName(f *framework.Framework) (string, func(*framework.TestCase)) {
 		secretKeyOne := fmt.Sprintf(namePrefix, f.Namespace.Name, "one")
 		secretKeyOne := fmt.Sprintf(namePrefix, f.Namespace.Name, "one")
 		secretKeyTwo := fmt.Sprintf(namePrefix, f.Namespace.Name, "two")
 		secretKeyTwo := fmt.Sprintf(namePrefix, f.Namespace.Name, "two")
 		secretKeyThree := fmt.Sprintf(namePrefix, f.Namespace.Name, "three")
 		secretKeyThree := fmt.Sprintf(namePrefix, f.Namespace.Name, "three")
-		secretValue := "{\"foo1\":\"foo1-val\"}"
+		secretValue := findValue
 		tc.Secrets = map[string]framework.SecretEntry{
 		tc.Secrets = map[string]framework.SecretEntry{
 			secretKeyOne:   {Value: secretValue},
 			secretKeyOne:   {Value: secretValue},
 			secretKeyTwo:   {Value: secretValue},
 			secretKeyTwo:   {Value: secretValue},
@@ -54,12 +58,56 @@ func FindByName(f *framework.Framework) (string, func(*framework.TestCase)) {
 	}
 	}
 }
 }
 
 
+// This case creates multiple secrets with simple key/value pairs and syncs them using multiple .Spec.Data blocks.
+func FindByNameAndRewrite(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return "[common] should find and rewrite secrets by name using .DataFrom[]", func(tc *framework.TestCase) {
+		const namePrefix = "e2e_find_and_rewrite_%s_%s"
+		secretKeyOne := fmt.Sprintf(namePrefix, f.Namespace.Name, "one")
+		secretKeyTwo := fmt.Sprintf(namePrefix, f.Namespace.Name, "two")
+		secretKeyThree := fmt.Sprintf(namePrefix, f.Namespace.Name, "three")
+		expectedKeyOne := fmt.Sprintf("%s_%s", f.Namespace.Name, "one")
+		expectedKeyTwo := fmt.Sprintf("%s_%s", f.Namespace.Name, "two")
+		expectedKeyThree := fmt.Sprintf("%s_%s", f.Namespace.Name, "three")
+		secretValue := findValue
+		tc.Secrets = map[string]framework.SecretEntry{
+			secretKeyOne:   {Value: secretValue},
+			secretKeyTwo:   {Value: secretValue},
+			secretKeyThree: {Value: secretValue},
+		}
+		tc.ExpectedSecret = &v1.Secret{
+			Type: v1.SecretTypeOpaque,
+			Data: map[string][]byte{
+				expectedKeyOne:   []byte(secretValue),
+				expectedKeyTwo:   []byte(secretValue),
+				expectedKeyThree: []byte(secretValue),
+			},
+		}
+		tc.ExternalSecret.Spec.DataFrom = []esapi.ExternalSecretDataFromRemoteRef{
+			{
+				Find: &esapi.ExternalSecretFind{
+					Name: &esapi.FindName{
+						RegExp: fmt.Sprintf("e2e_find_and_rewrite_%s.+", f.Namespace.Name),
+					},
+				},
+				Rewrite: []esapi.ExternalSecretRewrite{
+					{
+						Regexp: &esapi.ExternalSecretRewriteRegexp{
+							Source: "e2e_find_and_rewrite_(.*)",
+							Target: "$1",
+						},
+					},
+				},
+			},
+		}
+	}
+}
+
 func FindByNameWithPath(f *framework.Framework) (string, func(*framework.TestCase)) {
 func FindByNameWithPath(f *framework.Framework) (string, func(*framework.TestCase)) {
 	return "[common] should find secrets by name with path", func(tc *framework.TestCase) {
 	return "[common] should find secrets by name with path", func(tc *framework.TestCase) {
 		secretKeyOne := fmt.Sprintf("e2e-find-name-%s-one", f.Namespace.Name)
 		secretKeyOne := fmt.Sprintf("e2e-find-name-%s-one", f.Namespace.Name)
 		secretKeyTwo := fmt.Sprintf("%s-two", f.Namespace.Name)
 		secretKeyTwo := fmt.Sprintf("%s-two", f.Namespace.Name)
 		secretKeythree := fmt.Sprintf("%s-three", f.Namespace.Name)
 		secretKeythree := fmt.Sprintf("%s-three", f.Namespace.Name)
-		secretValue := "{\"foo1\":\"foo1-val\"}"
+		secretValue := findValue
 		tc.Secrets = map[string]framework.SecretEntry{
 		tc.Secrets = map[string]framework.SecretEntry{
 			secretKeyOne:   {Value: secretValue},
 			secretKeyOne:   {Value: secretValue},
 			secretKeyTwo:   {Value: secretValue},
 			secretKeyTwo:   {Value: secretValue},

+ 2 - 0
e2e/suites/provider/cases/gcp/gcp.go

@@ -38,6 +38,7 @@ var _ = Describe("[gcp]", Label("gcp", "secretsmanager"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataFromSync(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.DockerJSONConfig(f)),
 		Entry(common.DockerJSONConfig(f)),
@@ -47,6 +48,7 @@ var _ = Describe("[gcp]", Label("gcp", "secretsmanager"), func() {
 		Entry(common.SyncWithoutTargetName(f)),
 		Entry(common.SyncWithoutTargetName(f)),
 		Entry(common.JSONDataWithoutTargetName(f)),
 		Entry(common.JSONDataWithoutTargetName(f)),
 		Entry(common.FindByName(f)),
 		Entry(common.FindByName(f)),
+		Entry(common.FindByNameAndRewrite(f)),
 		Entry(common.FindByNameWithPath(f)),
 		Entry(common.FindByNameWithPath(f)),
 		Entry(common.FindByTag(f)),
 		Entry(common.FindByTag(f)),
 		Entry(common.FindByTagWithPath(f)),
 		Entry(common.FindByTagWithPath(f)),

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

@@ -34,6 +34,7 @@ var _ = Describe("[gitlab]", Label("gitlab"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataFromSync(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.SyncWithoutTargetName(f)),
 		Entry(common.SyncWithoutTargetName(f)),

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

@@ -41,6 +41,7 @@ var _ = Describe("[kubernetes] ", Label("kubernetes"), func() {
 		Entry(common.DataPropertyDockerconfigJSON(f)),
 		Entry(common.DataPropertyDockerconfigJSON(f)),
 		Entry(common.SSHKeySyncDataProperty(f)),
 		Entry(common.SSHKeySyncDataProperty(f)),
 		Entry(common.JSONDataFromSync(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(FindByTag(f)),
 		Entry(FindByTag(f)),
 		Entry(FindByName(f)),
 		Entry(FindByName(f)),
 
 

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

@@ -29,6 +29,7 @@ var _ = Describe("[oracle]", Label("oracle"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataFromSync(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.DockerJSONConfig(f)),
 		Entry(common.DockerJSONConfig(f)),

+ 12 - 0
e2e/suites/provider/cases/vault/vault.go

@@ -43,7 +43,9 @@ var _ = Describe("[vault]", Label("vault"), func() {
 		framework.TableFunc(f, prov),
 		framework.TableFunc(f, prov),
 		// uses token auth
 		// uses token auth
 		framework.Compose(withTokenAuth, f, common.FindByName, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.FindByName, useTokenAuth),
+		framework.Compose(withTokenAuth, f, common.FindByNameAndRewrite, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.JSONDataFromSync, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.JSONDataFromSync, useTokenAuth),
+		framework.Compose(withTokenAuth, f, common.JSONDataFromRewrite, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.JSONDataWithProperty, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.JSONDataWithProperty, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.JSONDataWithTemplate, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.JSONDataWithTemplate, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.DataPropertyDockerconfigJSON, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.DataPropertyDockerconfigJSON, useTokenAuth),
@@ -52,40 +54,50 @@ var _ = Describe("[vault]", Label("vault"), func() {
 		framework.Compose(withTokenAuth, f, common.DecodingPolicySync, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.DecodingPolicySync, useTokenAuth),
 		// use cert auth
 		// use cert auth
 		framework.Compose(withCertAuth, f, common.FindByName, useCertAuth),
 		framework.Compose(withCertAuth, f, common.FindByName, useCertAuth),
+		framework.Compose(withCertAuth, f, common.FindByNameAndRewrite, useCertAuth),
 		framework.Compose(withCertAuth, f, common.JSONDataFromSync, useCertAuth),
 		framework.Compose(withCertAuth, f, common.JSONDataFromSync, useCertAuth),
+		framework.Compose(withCertAuth, f, common.JSONDataFromRewrite, useCertAuth),
 		framework.Compose(withCertAuth, f, common.JSONDataWithProperty, useCertAuth),
 		framework.Compose(withCertAuth, f, common.JSONDataWithProperty, useCertAuth),
 		framework.Compose(withCertAuth, f, common.JSONDataWithTemplate, useCertAuth),
 		framework.Compose(withCertAuth, f, common.JSONDataWithTemplate, useCertAuth),
 		framework.Compose(withCertAuth, f, common.DataPropertyDockerconfigJSON, useCertAuth),
 		framework.Compose(withCertAuth, f, common.DataPropertyDockerconfigJSON, useCertAuth),
 		framework.Compose(withCertAuth, f, common.JSONDataWithoutTargetName, useCertAuth),
 		framework.Compose(withCertAuth, f, common.JSONDataWithoutTargetName, useCertAuth),
 		// use approle auth
 		// use approle auth
 		framework.Compose(withApprole, f, common.FindByName, useApproleAuth),
 		framework.Compose(withApprole, f, common.FindByName, useApproleAuth),
+		framework.Compose(withApprole, f, common.FindByNameAndRewrite, useApproleAuth),
 		framework.Compose(withApprole, f, common.JSONDataFromSync, useApproleAuth),
 		framework.Compose(withApprole, f, common.JSONDataFromSync, useApproleAuth),
+		framework.Compose(withApprole, f, common.JSONDataFromRewrite, useApproleAuth),
 		framework.Compose(withApprole, f, common.JSONDataWithProperty, useApproleAuth),
 		framework.Compose(withApprole, f, common.JSONDataWithProperty, useApproleAuth),
 		framework.Compose(withApprole, f, common.JSONDataWithTemplate, useApproleAuth),
 		framework.Compose(withApprole, f, common.JSONDataWithTemplate, useApproleAuth),
 		framework.Compose(withApprole, f, common.DataPropertyDockerconfigJSON, useApproleAuth),
 		framework.Compose(withApprole, f, common.DataPropertyDockerconfigJSON, useApproleAuth),
 		framework.Compose(withApprole, f, common.JSONDataWithoutTargetName, useApproleAuth),
 		framework.Compose(withApprole, f, common.JSONDataWithoutTargetName, useApproleAuth),
 		// use v1 provider
 		// use v1 provider
 		framework.Compose(withV1, f, common.JSONDataFromSync, useV1Provider),
 		framework.Compose(withV1, f, common.JSONDataFromSync, useV1Provider),
+		framework.Compose(withV1, f, common.JSONDataFromRewrite, useV1Provider),
 		framework.Compose(withV1, f, common.JSONDataWithProperty, useV1Provider),
 		framework.Compose(withV1, f, common.JSONDataWithProperty, useV1Provider),
 		framework.Compose(withV1, f, common.JSONDataWithTemplate, useV1Provider),
 		framework.Compose(withV1, f, common.JSONDataWithTemplate, useV1Provider),
 		framework.Compose(withV1, f, common.DataPropertyDockerconfigJSON, useV1Provider),
 		framework.Compose(withV1, f, common.DataPropertyDockerconfigJSON, useV1Provider),
 		framework.Compose(withV1, f, common.JSONDataWithoutTargetName, useV1Provider),
 		framework.Compose(withV1, f, common.JSONDataWithoutTargetName, useV1Provider),
 		// use jwt provider
 		// use jwt provider
 		framework.Compose(withJWT, f, common.FindByName, useJWTProvider),
 		framework.Compose(withJWT, f, common.FindByName, useJWTProvider),
+		framework.Compose(withJWT, f, common.FindByNameAndRewrite, useJWTProvider),
 		framework.Compose(withJWT, f, common.JSONDataFromSync, useJWTProvider),
 		framework.Compose(withJWT, f, common.JSONDataFromSync, useJWTProvider),
+		framework.Compose(withJWT, f, common.JSONDataFromRewrite, useJWTProvider),
 		framework.Compose(withJWT, f, common.JSONDataWithProperty, useJWTProvider),
 		framework.Compose(withJWT, f, common.JSONDataWithProperty, useJWTProvider),
 		framework.Compose(withJWT, f, common.JSONDataWithTemplate, useJWTProvider),
 		framework.Compose(withJWT, f, common.JSONDataWithTemplate, useJWTProvider),
 		framework.Compose(withJWT, f, common.DataPropertyDockerconfigJSON, useJWTProvider),
 		framework.Compose(withJWT, f, common.DataPropertyDockerconfigJSON, useJWTProvider),
 		framework.Compose(withJWT, f, common.JSONDataWithoutTargetName, useJWTProvider),
 		framework.Compose(withJWT, f, common.JSONDataWithoutTargetName, useJWTProvider),
 		// use jwt k8s provider
 		// use jwt k8s provider
 		framework.Compose(withJWTK8s, f, common.JSONDataFromSync, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.JSONDataFromSync, useJWTK8sProvider),
+		framework.Compose(withJWTK8s, f, common.JSONDataFromRewrite, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.JSONDataWithProperty, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.JSONDataWithProperty, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.JSONDataWithTemplate, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.JSONDataWithTemplate, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.DataPropertyDockerconfigJSON, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.DataPropertyDockerconfigJSON, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.JSONDataWithoutTargetName, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.JSONDataWithoutTargetName, useJWTK8sProvider),
 		// use kubernetes provider
 		// use kubernetes provider
 		framework.Compose(withK8s, f, common.FindByName, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.FindByName, useKubernetesProvider),
+		framework.Compose(withK8s, f, common.FindByNameAndRewrite, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.JSONDataFromSync, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.JSONDataFromSync, useKubernetesProvider),
+		framework.Compose(withK8s, f, common.JSONDataFromRewrite, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.JSONDataWithProperty, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.JSONDataWithProperty, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.JSONDataWithTemplate, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.JSONDataWithTemplate, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.DataPropertyDockerconfigJSON, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.DataPropertyDockerconfigJSON, useKubernetesProvider),

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

@@ -52,6 +52,7 @@ nav:
     - Getting Multiple Secrets: guides-getallsecrets.md
     - Getting Multiple Secrets: guides-getallsecrets.md
     - Multi Tenancy: guides-multi-tenancy.md
     - Multi Tenancy: guides-multi-tenancy.md
     - Metrics: guides-metrics.md
     - Metrics: guides-metrics.md
+    - Rewriting Keys: guides-datafrom-rewrite.md
     - Upgrading to v1beta1: guides-v1beta1.md
     - Upgrading to v1beta1: guides-v1beta1.md
     - Using Latest Image: guides-using-latest-image.md
     - Using Latest Image: guides-using-latest-image.md
   - Provider:
   - Provider:

+ 26 - 5
pkg/controllers/externalsecret/externalsecret_controller.go

@@ -51,6 +51,8 @@ const (
 	errGetES                 = "could not get ExternalSecret"
 	errGetES                 = "could not get ExternalSecret"
 	errConvert               = "could not apply conversion strategy to keys: %v"
 	errConvert               = "could not apply conversion strategy to keys: %v"
 	errDecode                = "could not apply decoding strategy to %v[%d]: %v"
 	errDecode                = "could not apply decoding strategy to %v[%d]: %v"
+	errRewrite               = "could not rewrite spec.dataFrom[%d]: %v"
+	errInvalidKeys           = "secret keys from spec.dataFrom.%v[%d] can only have alphanumeric,'-', '_' or '.' characters. Convert them using rewrite (https://external-secrets.io/latest/guides-datafrom-rewrite)"
 	errUpdateSecret          = "could not update Secret"
 	errUpdateSecret          = "could not update Secret"
 	errPatchStatus           = "unable to patch status"
 	errPatchStatus           = "unable to patch status"
 	errGetSecretStore        = "could not get SecretStore %q, %w"
 	errGetSecretStore        = "could not get SecretStore %q, %w"
@@ -533,9 +535,20 @@ func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient e
 			if err != nil {
 			if err != nil {
 				return nil, err
 				return nil, err
 			}
 			}
-			secretMap, err = utils.ConvertKeys(remoteRef.Find.ConversionStrategy, secretMap)
+			secretMap, err = utils.RewriteMap(remoteRef.Rewrite, secretMap)
 			if err != nil {
 			if err != nil {
-				return nil, fmt.Errorf(errConvert, err)
+				return nil, fmt.Errorf(errRewrite, i, err)
+			}
+			if len(remoteRef.Rewrite) == 0 {
+				// ConversionStrategy is deprecated. Use RewriteMap instead.
+				r.recorder.Event(externalSecret, v1.EventTypeWarning, esv1beta1.ReasonDeprecated, fmt.Sprintf("dataFrom[%d].find.conversionStrategy=%v is deprecated and will be removed in further releases. Use dataFrom.rewrite instead", i, remoteRef.Find.ConversionStrategy))
+				secretMap, err = utils.ConvertKeys(remoteRef.Find.ConversionStrategy, secretMap)
+				if err != nil {
+					return nil, fmt.Errorf(errConvert, err)
+				}
+			}
+			if !utils.ValidateKeys(secretMap) {
+				return nil, fmt.Errorf(errInvalidKeys, "find", i)
 			}
 			}
 			secretMap, err = utils.DecodeMap(remoteRef.Find.DecodingStrategy, secretMap)
 			secretMap, err = utils.DecodeMap(remoteRef.Find.DecodingStrategy, secretMap)
 			if err != nil {
 			if err != nil {
@@ -550,16 +563,24 @@ func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient e
 			if err != nil {
 			if err != nil {
 				return nil, err
 				return nil, err
 			}
 			}
-			secretMap, err = utils.ConvertKeys(remoteRef.Extract.ConversionStrategy, secretMap)
+			secretMap, err = utils.RewriteMap(remoteRef.Rewrite, secretMap)
 			if err != nil {
 			if err != nil {
-				return nil, fmt.Errorf(errConvert, err)
+				return nil, fmt.Errorf(errRewrite, i, err)
+			}
+			if len(remoteRef.Rewrite) == 0 {
+				secretMap, err = utils.ConvertKeys(remoteRef.Extract.ConversionStrategy, secretMap)
+				if err != nil {
+					return nil, fmt.Errorf(errConvert, err)
+				}
+			}
+			if !utils.ValidateKeys(secretMap) {
+				return nil, fmt.Errorf(errInvalidKeys, "extract", i)
 			}
 			}
 			secretMap, err = utils.DecodeMap(remoteRef.Extract.DecodingStrategy, secretMap)
 			secretMap, err = utils.DecodeMap(remoteRef.Extract.DecodingStrategy, secretMap)
 			if err != nil {
 			if err != nil {
 				return nil, fmt.Errorf(errDecode, "spec.dataFrom", i, err)
 				return nil, fmt.Errorf(errDecode, "spec.dataFrom", i, err)
 			}
 			}
 		}
 		}
-
 		providerData = utils.MergeByteMap(providerData, secretMap)
 		providerData = utils.MergeByteMap(providerData, secretMap)
 	}
 	}
 
 

+ 168 - 0
pkg/controllers/externalsecret/externalsecret_controller_test.go

@@ -945,6 +945,139 @@ var _ = Describe("ExternalSecret controller", func() {
 		}
 		}
 	}
 	}
 
 
+	// with rewrite all keys from a dataFrom operation
+	// should be put with new rewriting into the secret
+	syncAndRewriteWithDataFrom := func(tc *testCase) {
+		tc.externalSecret.Spec.Data = nil
+		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
+					Key: remoteKey,
+				},
+				Rewrite: []esv1beta1.ExternalSecretRewrite{{
+					Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+						Source: "(.*)",
+						Target: "new-$1",
+					},
+				}},
+			},
+			{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
+					Key: remoteKey,
+				},
+				Rewrite: []esv1beta1.ExternalSecretRewrite{{
+					Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+						Source: "(.*)",
+						Target: "old-$1",
+					},
+				}},
+			},
+		}
+		fakeProvider.WithGetSecretMap(map[string][]byte{
+			"foo": []byte(FooValue),
+			"bar": []byte(BarValue),
+		}, nil)
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			// check values
+			Expect(string(secret.Data["new-foo"])).To(Equal(FooValue))
+			Expect(string(secret.Data["new-bar"])).To(Equal(BarValue))
+			Expect(string(secret.Data["old-foo"])).To(Equal(FooValue))
+			Expect(string(secret.Data["old-bar"])).To(Equal(BarValue))
+		}
+	}
+	// with rewrite keys from dataFrom
+	// should error if keys are not compliant
+	invalidExtractKeysErrCondition := func(tc *testCase) {
+		tc.externalSecret.Spec.Data = nil
+		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
+					Key: remoteKey,
+				},
+				Rewrite: []esv1beta1.ExternalSecretRewrite{{
+					Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+						Source: "(.*)",
+						Target: "$1",
+					},
+				}},
+			},
+		}
+		fakeProvider.WithGetSecretMap(map[string][]byte{
+			"foo/bar": []byte(FooValue),
+			"bar/foo": []byte(BarValue),
+		}, nil)
+		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
+			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError {
+				return false
+			}
+			return true
+		}
+		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
+			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError {
+				return false
+			}
+			return true
+		}
+		tc.checkExternalSecret = func(es *esv1beta1.ExternalSecret) {
+			Eventually(func() bool {
+				Expect(syncCallsError.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
+				return metric.GetCounter().GetValue() >= 2.0
+			}, timeout, interval).Should(BeTrue())
+			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1beta1.ExternalSecretReady, v1.ConditionFalse, 1.0)).To(BeTrue())
+			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1beta1.ExternalSecretReady, v1.ConditionTrue, 0.0)).To(BeTrue())
+		}
+
+	}
+	// with rewrite keys from dataFrom
+	// should error if keys are not compliant
+	invalidFindKeysErrCondition := func(tc *testCase) {
+		tc.externalSecret.Spec.Data = nil
+		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				Find: &esv1beta1.ExternalSecretFind{
+					Name: &esv1beta1.FindName{
+						RegExp: ".*",
+					},
+				},
+				Rewrite: []esv1beta1.ExternalSecretRewrite{{
+					Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+						Source: "(.*)",
+						Target: "$1",
+					},
+				}},
+			},
+		}
+		fakeProvider.WithGetAllSecrets(map[string][]byte{
+			"foo/bar": []byte(FooValue),
+			"bar/foo": []byte(BarValue),
+		}, nil)
+		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
+			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError {
+				return false
+			}
+			return true
+		}
+		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
+			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError {
+				return false
+			}
+			return true
+		}
+		tc.checkExternalSecret = func(es *esv1beta1.ExternalSecret) {
+			Eventually(func() bool {
+				Expect(syncCallsError.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
+				return metric.GetCounter().GetValue() >= 2.0
+			}, timeout, interval).Should(BeTrue())
+			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1beta1.ExternalSecretReady, v1.ConditionFalse, 1.0)).To(BeTrue())
+			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1beta1.ExternalSecretReady, v1.ConditionTrue, 0.0)).To(BeTrue())
+		}
+
+	}
+
 	// with dataFrom all properties from the specified secret
 	// with dataFrom all properties from the specified secret
 	// should be put into the secret
 	// should be put into the secret
 	syncWithDataFrom := func(tc *testCase) {
 	syncWithDataFrom := func(tc *testCase) {
@@ -966,6 +1099,37 @@ var _ = Describe("ExternalSecret controller", func() {
 			Expect(string(secret.Data["bar"])).To(Equal(BarValue))
 			Expect(string(secret.Data["bar"])).To(Equal(BarValue))
 		}
 		}
 	}
 	}
+	// with dataFrom.Find the change is on the called method GetAllSecrets
+	// all keys should be put into the secret
+	syncAndRewriteDataFromFind := func(tc *testCase) {
+		tc.externalSecret.Spec.Data = nil
+		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				Find: &esv1beta1.ExternalSecretFind{
+					Name: &esv1beta1.FindName{
+						RegExp: "foobar",
+					},
+				},
+				Rewrite: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "(.*)",
+							Target: "new-$1",
+						},
+					},
+				},
+			},
+		}
+		fakeProvider.WithGetAllSecrets(map[string][]byte{
+			"foo": []byte(FooValue),
+			"bar": []byte(BarValue),
+		}, nil)
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			// check values
+			Expect(string(secret.Data["new-foo"])).To(Equal(FooValue))
+			Expect(string(secret.Data["new-bar"])).To(Equal(BarValue))
+		}
+	}
 
 
 	// with dataFrom.Find the change is on the called method GetAllSecrets
 	// with dataFrom.Find the change is on the called method GetAllSecrets
 	// all keys should be put into the secret
 	// all keys should be put into the secret
@@ -1284,7 +1448,11 @@ var _ = Describe("ExternalSecret controller", func() {
 		Entry("should refresh secret map when provider secret changes when using a template", refreshSecretValueMapTemplate),
 		Entry("should refresh secret map when provider secret changes when using a template", refreshSecretValueMapTemplate),
 		Entry("should not refresh secret value when provider secret changes but refreshInterval is zero", refreshintervalZero),
 		Entry("should not refresh secret value when provider secret changes but refreshInterval is zero", refreshintervalZero),
 		Entry("should fetch secret using dataFrom", syncWithDataFrom),
 		Entry("should fetch secret using dataFrom", syncWithDataFrom),
+		Entry("should rewrite secret using dataFrom", syncAndRewriteWithDataFrom),
+		Entry("should not automatically convert from extract if rewrite is used", invalidExtractKeysErrCondition),
 		Entry("should fetch secret using dataFrom.find", syncDataFromFind),
 		Entry("should fetch secret using dataFrom.find", syncDataFromFind),
+		Entry("should rewrite secret using dataFrom.find", syncAndRewriteDataFromFind),
+		Entry("should not automatically convert from find if rewrite is used", invalidFindKeysErrCondition),
 		Entry("should fetch secret using dataFrom and a template", syncWithDataFromTemplate),
 		Entry("should fetch secret using dataFrom and a template", syncWithDataFromTemplate),
 		Entry("should set error condition when provider errors", providerErrCondition),
 		Entry("should set error condition when provider errors", providerErrCondition),
 		Entry("should set an error condition when store does not exist", storeMissingErrCondition),
 		Entry("should set an error condition when store does not exist", storeMissingErrCondition),

+ 3 - 0
pkg/provider/vault/vault.go

@@ -429,6 +429,9 @@ func (v *client) listSecrets(ctx context.Context, path string) ([]string, error)
 		return nil, err
 		return nil, err
 	}
 	}
 	secret, err := v.logical.ListWithContext(ctx, url)
 	secret, err := v.logical.ListWithContext(ctx, url)
+	if secret == nil {
+		return nil, fmt.Errorf("provided path %v does not contain any secrets", url)
+	}
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf(errReadSecret, err)
 		return nil, fmt.Errorf(errReadSecret, err)
 	}
 	}

+ 46 - 0
pkg/utils/utils.go

@@ -24,6 +24,7 @@ import (
 	"net"
 	"net"
 	"net/url"
 	"net/url"
 	"reflect"
 	"reflect"
+	"regexp"
 	"strings"
 	"strings"
 	"time"
 	"time"
 	"unicode"
 	"unicode"
@@ -40,6 +41,34 @@ func MergeByteMap(dst, src map[string][]byte) map[string][]byte {
 	return dst
 	return dst
 }
 }
 
 
+func RewriteMap(operations []esv1beta1.ExternalSecretRewrite, in map[string][]byte) (map[string][]byte, error) {
+	out := in
+	var err error
+	for i, op := range operations {
+		if op.Regexp != nil {
+			out, err = RewriteRegexp(*op.Regexp, out)
+			if err != nil {
+				return nil, fmt.Errorf("failed rewriting operation[%v]: %w", i, err)
+			}
+		}
+	}
+	return out, nil
+}
+
+// RewriteRegexp rewrites a single Regexp Rewrite Operation.
+func RewriteRegexp(operation esv1beta1.ExternalSecretRewriteRegexp, in map[string][]byte) (map[string][]byte, error) {
+	out := make(map[string][]byte)
+	re, err := regexp.Compile(operation.Source)
+	if err != nil {
+		return nil, err
+	}
+	for key, value := range in {
+		newKey := re.ReplaceAllString(key, operation.Target)
+		out[newKey] = value
+	}
+	return out, nil
+}
+
 // DecodeValues decodes values from a secretMap.
 // DecodeValues decodes values from a secretMap.
 func DecodeMap(strategy esv1beta1.ExternalSecretDecodingStrategy, in map[string][]byte) (map[string][]byte, error) {
 func DecodeMap(strategy esv1beta1.ExternalSecretDecodingStrategy, in map[string][]byte) (map[string][]byte, error) {
 	out := make(map[string][]byte, len(in))
 	out := make(map[string][]byte, len(in))
@@ -87,6 +116,21 @@ func Decode(strategy esv1beta1.ExternalSecretDecodingStrategy, in []byte) ([]byt
 	}
 	}
 }
 }
 
 
+func ValidateKeys(in map[string][]byte) bool {
+	for key := range in {
+		for _, v := range key {
+			if !unicode.IsNumber(v) &&
+				!unicode.IsLetter(v) &&
+				v != '-' &&
+				v != '.' &&
+				v != '_' {
+				return false
+			}
+		}
+	}
+	return true
+}
+
 // ConvertKeys converts a secret map into a valid key.
 // ConvertKeys converts a secret map into a valid key.
 // Replaces any non-alphanumeric characters depending on convert strategy.
 // Replaces any non-alphanumeric characters depending on convert strategy.
 func ConvertKeys(strategy esv1beta1.ExternalSecretConversionStrategy, in map[string][]byte) (map[string][]byte, error) {
 func ConvertKeys(strategy esv1beta1.ExternalSecretConversionStrategy, in map[string][]byte) (map[string][]byte, error) {
@@ -115,6 +159,8 @@ func convert(strategy esv1beta1.ExternalSecretConversionStrategy, str string) st
 				newName[rk] = "_"
 				newName[rk] = "_"
 			case esv1beta1.ExternalSecretConversionUnicode:
 			case esv1beta1.ExternalSecretConversionUnicode:
 				newName[rk] = fmt.Sprintf("_U%04x_", rv)
 				newName[rk] = fmt.Sprintf("_U%04x_", rv)
+			default:
+				newName[rk] = string(rv)
 			}
 			}
 		} else {
 		} else {
 			newName[rk] = string(rv)
 			newName[rk] = string(rv)

+ 158 - 0
pkg/utils/utils_test.go

@@ -335,3 +335,161 @@ func TestValidate(t *testing.T) {
 		t.Errorf("Connection problem: %v", err)
 		t.Errorf("Connection problem: %v", err)
 	}
 	}
 }
 }
+
+func TestRewriteRegexp(t *testing.T) {
+	type args struct {
+		operations []esv1beta1.ExternalSecretRewrite
+		in         map[string][]byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    map[string][]byte
+		wantErr bool
+	}{
+		{
+			name: "replace of a single key",
+			args: args{
+				operations: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "-",
+							Target: "_",
+						},
+					},
+				},
+				in: map[string][]byte{
+					"foo-bar": []byte("bar"),
+				},
+			},
+			want: map[string][]byte{
+				"foo_bar": []byte("bar"),
+			},
+		},
+		{
+			name: "no operation",
+			args: args{
+				operations: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "hello",
+							Target: "world",
+						},
+					},
+				},
+				in: map[string][]byte{
+					"foo": []byte("bar"),
+				},
+			},
+			want: map[string][]byte{
+				"foo": []byte("bar"),
+			},
+		},
+		{
+			name: "removing prefix from keys",
+			args: args{
+				operations: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "^my/initial/path/",
+							Target: "",
+						},
+					},
+				},
+				in: map[string][]byte{
+					"my/initial/path/foo": []byte("bar"),
+				},
+			},
+			want: map[string][]byte{
+				"foo": []byte("bar"),
+			},
+		},
+		{
+			name: "using un-named capture groups",
+			args: args{
+				operations: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "f(.*)o",
+							Target: "a_new_path_$1",
+						},
+					},
+				},
+				in: map[string][]byte{
+					"foo":      []byte("bar"),
+					"foodaloo": []byte("barr"),
+				},
+			},
+			want: map[string][]byte{
+				"a_new_path_o":      []byte("bar"),
+				"a_new_path_oodalo": []byte("barr"),
+			},
+		},
+		{
+			name: "using named and numbered capture groups",
+			args: args{
+				operations: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "f(?P<content>.*)o",
+							Target: "a_new_path_${content}_${1}",
+						},
+					},
+				},
+				in: map[string][]byte{
+					"foo":  []byte("bar"),
+					"floo": []byte("barr"),
+				},
+			},
+			want: map[string][]byte{
+				"a_new_path_o_o":   []byte("bar"),
+				"a_new_path_lo_lo": []byte("barr"),
+			},
+		},
+		{
+			name: "using sequenced rewrite operations",
+			args: args{
+				operations: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "my/(.*?)/bar/(.*)",
+							Target: "$1-$2",
+						},
+					},
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "-",
+							Target: "_",
+						},
+					},
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "ass",
+							Target: "***",
+						},
+					},
+				},
+				in: map[string][]byte{
+					"my/app/bar/key":      []byte("bar"),
+					"my/app/bar/password": []byte("barr"),
+				},
+			},
+			want: map[string][]byte{
+				"app_key":      []byte("bar"),
+				"app_p***word": []byte("barr"),
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := RewriteMap(tt.args.operations, tt.args.in)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("RewriteMap() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("RewriteMap() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}