mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
feat: add templating to PushSecret (#2926)
* feat: add templating to PushSecret Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> * adding unit tests around templating basic concepts and verifying output Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> * extracting some of the common functions of the parser Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> * remove some more duplication Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> * removed commented out code segment Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> * added documentation for templating feature Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> * simplified the templating for annotations and labels Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> --------- Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
This commit is contained in:
parent
9130719b20
commit
d6e24a82bd
13 changed files with 676 additions and 188 deletions
|
@ -18,6 +18,8 @@ import (
|
|||
corev1 "k8s.io/api/core/v1"
|
||||
apiextensionsv1 "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"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -60,6 +62,9 @@ type PushSecretSpec struct {
|
|||
Selector PushSecretSelector `json:"selector"`
|
||||
// Secret Data that should be pushed to providers
|
||||
Data []PushSecretData `json:"data,omitempty"`
|
||||
// Template defines a blueprint for the created Secret resource.
|
||||
// +optional
|
||||
Template *esv1beta1.ExternalSecretTemplate `json:"template,omitempty"`
|
||||
}
|
||||
|
||||
type PushSecretSecret struct {
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
package v1alpha1
|
||||
|
||||
import (
|
||||
"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||
metav1 "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
@ -1195,6 +1196,11 @@ func (in *PushSecretSpec) DeepCopyInto(out *PushSecretSpec) {
|
|||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Template != nil {
|
||||
in, out := &in.Template, &out.Template
|
||||
*out = new(v1beta1.ExternalSecretTemplate)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretSpec.
|
||||
|
|
|
@ -162,6 +162,104 @@ spec:
|
|||
required:
|
||||
- secret
|
||||
type: object
|
||||
template:
|
||||
description: Template defines a blueprint for the created Secret resource.
|
||||
properties:
|
||||
data:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
engineVersion:
|
||||
default: v2
|
||||
description: EngineVersion specifies the template engine version
|
||||
that should be used to compile/execute the template specified
|
||||
in .data and .templateFrom[].
|
||||
enum:
|
||||
- v1
|
||||
- v2
|
||||
type: string
|
||||
mergePolicy:
|
||||
default: Replace
|
||||
enum:
|
||||
- Replace
|
||||
- Merge
|
||||
type: string
|
||||
metadata:
|
||||
description: ExternalSecretTemplateMetadata defines metadata fields
|
||||
for the Secret blueprint.
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
templateFrom:
|
||||
items:
|
||||
properties:
|
||||
configMap:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
templateAs:
|
||||
default: Values
|
||||
enum:
|
||||
- Values
|
||||
- KeysAndValues
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
type: object
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- items
|
||||
- name
|
||||
type: object
|
||||
literal:
|
||||
type: string
|
||||
secret:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
templateAs:
|
||||
default: Values
|
||||
enum:
|
||||
- Values
|
||||
- KeysAndValues
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
type: object
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- items
|
||||
- name
|
||||
type: object
|
||||
target:
|
||||
default: Data
|
||||
enum:
|
||||
- Data
|
||||
- Annotations
|
||||
- Labels
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- secretStoreRefs
|
||||
- selector
|
||||
|
|
|
@ -4384,6 +4384,101 @@ spec:
|
|||
required:
|
||||
- secret
|
||||
type: object
|
||||
template:
|
||||
description: Template defines a blueprint for the created Secret resource.
|
||||
properties:
|
||||
data:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
engineVersion:
|
||||
default: v2
|
||||
description: EngineVersion specifies the template engine version that should be used to compile/execute the template specified in .data and .templateFrom[].
|
||||
enum:
|
||||
- v1
|
||||
- v2
|
||||
type: string
|
||||
mergePolicy:
|
||||
default: Replace
|
||||
enum:
|
||||
- Replace
|
||||
- Merge
|
||||
type: string
|
||||
metadata:
|
||||
description: ExternalSecretTemplateMetadata defines metadata fields for the Secret blueprint.
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
templateFrom:
|
||||
items:
|
||||
properties:
|
||||
configMap:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
templateAs:
|
||||
default: Values
|
||||
enum:
|
||||
- Values
|
||||
- KeysAndValues
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
type: object
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- items
|
||||
- name
|
||||
type: object
|
||||
literal:
|
||||
type: string
|
||||
secret:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
templateAs:
|
||||
default: Values
|
||||
enum:
|
||||
- Values
|
||||
- KeysAndValues
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
type: object
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- items
|
||||
- name
|
||||
type: object
|
||||
target:
|
||||
default: Data
|
||||
enum:
|
||||
- Data
|
||||
- Annotations
|
||||
- Labels
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- secretStoreRefs
|
||||
- selector
|
||||
|
|
|
@ -3,8 +3,16 @@
|
|||
The `PushSecret` is namespaced and it describes what data should be pushed to the secret provider.
|
||||
|
||||
* tells the operator what secrets should be pushed by using `spec.selector`.
|
||||
* you can specify what secret keys should be pushed by using `spec.data`
|
||||
* you can specify what secret keys should be pushed by using `spec.data`.
|
||||
* you can also template the resulting property values using [templating](#templating).
|
||||
|
||||
``` yaml
|
||||
{% include 'full-pushsecret.yaml' %}
|
||||
```
|
||||
|
||||
## Templating
|
||||
|
||||
When the controller reconciles the `PushSecret` it will use the `spec.template` as a blueprint to construct a new property.
|
||||
You can use golang templates to define the blueprint and use template functions to transform the defined properties.
|
||||
You can also pull in `ConfigMaps` that contain golang-template data using `templateFrom`.
|
||||
See [advanced templating](../guides/templating.md) for details.
|
||||
|
|
|
@ -112,6 +112,16 @@ You can achieve that by using the `filterPEM` function to extract a specific typ
|
|||
{% include 'filterpem-template-v2-external-secret.yaml' %}
|
||||
```
|
||||
|
||||
## Templating with PushSecret
|
||||
|
||||
`PushSecret` templating is much like `ExternalSecrets` templating. In-fact under the hood, it's using the same data structure.
|
||||
Which means, anything described in the above should be possible with push secret as well resulting in a templated secret
|
||||
created at the provider.
|
||||
|
||||
```yaml
|
||||
{% include 'template-v2-push-secret.yaml' %}
|
||||
```
|
||||
|
||||
## Helper functions
|
||||
|
||||
!!! info inline end
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{% raw %}
|
||||
apiVersion: external-secrets.io/v1alpha1
|
||||
kind: PushSecret
|
||||
metadata:
|
||||
|
@ -12,8 +13,23 @@ spec:
|
|||
selector:
|
||||
secret:
|
||||
name: pokedex-credentials # Source Kubernetes secret to be pushed
|
||||
template:
|
||||
metadata:
|
||||
annotations: { }
|
||||
labels: { }
|
||||
data:
|
||||
best-pokemon: "{{ .best-pokemon | toString | upper }} is the really best!"
|
||||
# Uses an existing template from configmap
|
||||
# Secret is fetched, merged and templated within the referenced configMap data
|
||||
# It does not update the configmap, it creates a secret with: data["alertmanager.yml"] = ...result...
|
||||
templateFrom:
|
||||
- configMap:
|
||||
name: application-config-tmpl
|
||||
items:
|
||||
- key: config.yml
|
||||
data:
|
||||
- match:
|
||||
secretKey: best-pokemon # Source Kubernetes secret key to be pushed
|
||||
remoteRef:
|
||||
remoteKey: my-first-parameter # Remote reference (where the secret is going to be pushed)
|
||||
{% endraw %}
|
||||
|
|
18
docs/snippets/template-v2-push-secret.yaml
Normal file
18
docs/snippets/template-v2-push-secret.yaml
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% raw %}
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: PushSecret
|
||||
metadata:
|
||||
name: template
|
||||
spec:
|
||||
# ...
|
||||
template:
|
||||
engineVersion: v2
|
||||
data:
|
||||
token: "{{ .token | toString | upper }} was templated"
|
||||
data:
|
||||
- match:
|
||||
secretKey: token
|
||||
remoteRef:
|
||||
remoteKey: create-secret-name
|
||||
property: token
|
||||
{% endraw %}
|
|
@ -19,129 +19,14 @@ import (
|
|||
"fmt"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||
// Loading registered providers.
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/register"
|
||||
"github.com/external-secrets/external-secrets/pkg/controllers/templating"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/register" // Loading registered providers.
|
||||
"github.com/external-secrets/external-secrets/pkg/template"
|
||||
"github.com/external-secrets/external-secrets/pkg/utils"
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
exec template.ExecFunc
|
||||
dataMap map[string][]byte
|
||||
client client.Client
|
||||
targetSecret *v1.Secret
|
||||
}
|
||||
|
||||
func (p *Parser) MergeConfigMap(ctx context.Context, namespace string, tpl esv1beta1.TemplateFrom) error {
|
||||
if tpl.ConfigMap == nil {
|
||||
return nil
|
||||
}
|
||||
var cm v1.ConfigMap
|
||||
err := p.client.Get(ctx, types.NamespacedName{
|
||||
Name: tpl.ConfigMap.Name,
|
||||
Namespace: namespace,
|
||||
}, &cm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, k := range tpl.ConfigMap.Items {
|
||||
val, ok := cm.Data[k.Key]
|
||||
out := make(map[string][]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf(errTplCMMissingKey, tpl.ConfigMap.Name, k.Key)
|
||||
}
|
||||
switch k.TemplateAs {
|
||||
case esv1beta1.TemplateScopeValues:
|
||||
out[k.Key] = []byte(val)
|
||||
case esv1beta1.TemplateScopeKeysAndValues:
|
||||
out[val] = []byte(val)
|
||||
}
|
||||
err = p.exec(out, p.dataMap, k.TemplateAs, tpl.Target, p.targetSecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) MergeSecret(ctx context.Context, namespace string, tpl esv1beta1.TemplateFrom) error {
|
||||
if tpl.Secret == nil {
|
||||
return nil
|
||||
}
|
||||
var sec v1.Secret
|
||||
err := p.client.Get(ctx, types.NamespacedName{
|
||||
Name: tpl.Secret.Name,
|
||||
Namespace: namespace,
|
||||
}, &sec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, k := range tpl.Secret.Items {
|
||||
val, ok := sec.Data[k.Key]
|
||||
if !ok {
|
||||
return fmt.Errorf(errTplSecMissingKey, tpl.Secret.Name, k.Key)
|
||||
}
|
||||
out := make(map[string][]byte)
|
||||
switch k.TemplateAs {
|
||||
case esv1beta1.TemplateScopeValues:
|
||||
out[k.Key] = val
|
||||
case esv1beta1.TemplateScopeKeysAndValues:
|
||||
out[string(val)] = val
|
||||
}
|
||||
err = p.exec(out, p.dataMap, k.TemplateAs, tpl.Target, p.targetSecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) MergeLiteral(_ context.Context, tpl esv1beta1.TemplateFrom) error {
|
||||
if tpl.Literal == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string][]byte)
|
||||
out[*tpl.Literal] = []byte(*tpl.Literal)
|
||||
return p.exec(out, p.dataMap, esv1beta1.TemplateScopeKeysAndValues, tpl.Target, p.targetSecret)
|
||||
}
|
||||
|
||||
func (p *Parser) MergeTemplateFrom(ctx context.Context, es *esv1beta1.ExternalSecret) error {
|
||||
if es.Spec.Target.Template == nil {
|
||||
return nil
|
||||
}
|
||||
for _, tpl := range es.Spec.Target.Template.TemplateFrom {
|
||||
err := p.MergeConfigMap(ctx, es.Namespace, tpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.MergeSecret(ctx, es.Namespace, tpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.MergeLiteral(ctx, tpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) MergeMap(tplMap map[string]string, target esv1beta1.TemplateTarget) error {
|
||||
byteMap := make(map[string][]byte)
|
||||
for k, v := range tplMap {
|
||||
byteMap[k] = []byte(v)
|
||||
}
|
||||
err := p.exec(byteMap, p.dataMap, esv1beta1.TemplateScopeValues, target, p.targetSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errExecTpl, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// merge template in the following order:
|
||||
// * template.Data (highest precedence)
|
||||
// * template.templateFrom
|
||||
|
@ -167,14 +52,14 @@ func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1beta1.ExternalSe
|
|||
return err
|
||||
}
|
||||
|
||||
p := Parser{
|
||||
client: r.Client,
|
||||
targetSecret: secret,
|
||||
dataMap: dataMap,
|
||||
exec: execute,
|
||||
p := templating.Parser{
|
||||
Client: r.Client,
|
||||
TargetSecret: secret,
|
||||
DataMap: dataMap,
|
||||
Exec: execute,
|
||||
}
|
||||
// apply templates defined in template.templateFrom
|
||||
err = p.MergeTemplateFrom(ctx, es)
|
||||
err = p.MergeTemplateFrom(ctx, es.Namespace, es.Spec.Target.Template)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errFetchTplFrom, err)
|
||||
}
|
||||
|
@ -212,7 +97,7 @@ func setMetadata(secret *v1.Secret, es *esv1beta1.ExternalSecret) error {
|
|||
}
|
||||
// Clean up Labels and Annotations added by the operator
|
||||
// so that it won't leave outdated ones
|
||||
labelKeys, err := getManagedLabelKeys(secret, es.Name)
|
||||
labelKeys, err := templating.GetManagedLabelKeys(secret, es.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -220,7 +105,7 @@ func setMetadata(secret *v1.Secret, es *esv1beta1.ExternalSecret) error {
|
|||
delete(secret.ObjectMeta.Labels, key)
|
||||
}
|
||||
|
||||
annotationKeys, err := getManagedAnnotationKeys(secret, es.Name)
|
||||
annotationKeys, err := templating.GetManagedAnnotationKeys(secret, es.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -239,55 +124,3 @@ func setMetadata(secret *v1.Secret, es *esv1beta1.ExternalSecret) error {
|
|||
utils.MergeStringMap(secret.ObjectMeta.Annotations, es.Spec.Target.Template.Metadata.Annotations)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getManagedAnnotationKeys(secret *v1.Secret, fieldOwner string) ([]string, error) {
|
||||
return getManagedFieldKeys(secret, fieldOwner, func(fields map[string]interface{}) []string {
|
||||
metadataFields, exists := fields["f:metadata"]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
mf, ok := metadataFields.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
annotationFields, exists := mf["f:annotations"]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
af, ok := annotationFields.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var keys []string
|
||||
for k := range af {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
})
|
||||
}
|
||||
|
||||
func getManagedLabelKeys(secret *v1.Secret, fieldOwner string) ([]string, error) {
|
||||
return getManagedFieldKeys(secret, fieldOwner, func(fields map[string]interface{}) []string {
|
||||
metadataFields, exists := fields["f:metadata"]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
mf, ok := metadataFields.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
labelFields, exists := mf["f:labels"]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
lf, ok := labelFields.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var keys []string
|
||||
for k := range lf {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
})
|
||||
}
|
||||
|
|
|
@ -45,9 +45,6 @@ const (
|
|||
errPatchStatus = "error merging"
|
||||
errGetSecretStore = "could not get SecretStore %q, %w"
|
||||
errGetClusterSecretStore = "could not get ClusterSecretStore %q, %w"
|
||||
errGetProviderFailed = "could not start provider"
|
||||
errGetSecretsClientFailed = "could not start secrets client"
|
||||
errCloseStoreClient = "error when calling provider close method"
|
||||
errSetSecretFailed = "could not write remote ref %v to target secretstore %v: %v"
|
||||
errFailedSetSecret = "set secret failed: %v"
|
||||
pushSecretFinalizer = "pushsecret.externalsecrets.io/finalizer"
|
||||
|
@ -153,6 +150,11 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
|
|||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if err := r.applyTemplate(ctx, &ps, secret); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
syncedSecrets, err := r.PushSecretToProviders(ctx, secretStores, ps, secret, mgr)
|
||||
if err != nil {
|
||||
if errors.Is(err, locks.ErrConflict) {
|
||||
|
|
104
pkg/controllers/pushsecret/pushsecret_controller_template.go
Normal file
104
pkg/controllers/pushsecret/pushsecret_controller_template.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
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 pushsecret
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
|
||||
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||
"github.com/external-secrets/external-secrets/pkg/controllers/templating"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/register" // Loading registered providers.
|
||||
"github.com/external-secrets/external-secrets/pkg/template"
|
||||
"github.com/external-secrets/external-secrets/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
errFetchTplFrom = "error fetching templateFrom data: %w"
|
||||
errExecTpl = "could not execute template: %w"
|
||||
)
|
||||
|
||||
// applyTemplate merges template in the following order:
|
||||
// * template.Data (highest precedence)
|
||||
// * template.templateFrom
|
||||
// * secret via ps.data or ps.dataFrom.
|
||||
// Apply template modifications for the source secret. These modifications will only live in memory as we will
|
||||
// never modify it.
|
||||
func (r *Reconciler) applyTemplate(ctx context.Context, ps *v1alpha1.PushSecret, secret *v1.Secret) error {
|
||||
// no template: nothing to do
|
||||
if ps.Spec.Template == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := setMetadata(secret, ps); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
execute, err := template.EngineForVersion(esv1beta1.TemplateEngineV2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := templating.Parser{
|
||||
Client: r.Client,
|
||||
TargetSecret: secret,
|
||||
DataMap: secret.Data,
|
||||
Exec: execute,
|
||||
}
|
||||
|
||||
// apply templates defined in template.templateFrom
|
||||
err = p.MergeTemplateFrom(ctx, ps.Namespace, ps.Spec.Template)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errFetchTplFrom, err)
|
||||
}
|
||||
// explicitly defined template.Data takes precedence over templateFrom
|
||||
err = p.MergeMap(ps.Spec.Template.Data, esv1beta1.TemplateTargetData)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errExecTpl, err)
|
||||
}
|
||||
|
||||
// get template data for labels
|
||||
err = p.MergeMap(ps.Spec.Template.Metadata.Labels, esv1beta1.TemplateTargetLabels)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errExecTpl, err)
|
||||
}
|
||||
// get template data for annotations
|
||||
err = p.MergeMap(ps.Spec.Template.Metadata.Annotations, esv1beta1.TemplateTargetAnnotations)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errExecTpl, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setMetadata sets Labels and Annotations in the source secret, but we will never write them back.
|
||||
// It is only set to satisfy templated changes.
|
||||
func setMetadata(secret *v1.Secret, ps *v1alpha1.PushSecret) error {
|
||||
if secret.Labels == nil {
|
||||
secret.Labels = make(map[string]string)
|
||||
}
|
||||
if secret.Annotations == nil {
|
||||
secret.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
secret.Type = ps.Spec.Template.Type
|
||||
utils.MergeStringMap(secret.ObjectMeta.Labels, ps.Spec.Template.Metadata.Labels)
|
||||
utils.MergeStringMap(secret.ObjectMeta.Annotations, ps.Spec.Template.Metadata.Annotations)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -206,6 +206,69 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// if target Secret name is not specified it should use the ExternalSecret name.
|
||||
syncSuccessfullyWithTemplate := func(tc *testCase) {
|
||||
fakeProvider.SetSecretFn = func() error {
|
||||
return nil
|
||||
}
|
||||
tc.pushsecret = &v1alpha1.PushSecret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: PushSecretName,
|
||||
Namespace: PushSecretNamespace,
|
||||
},
|
||||
Spec: v1alpha1.PushSecretSpec{
|
||||
SecretStoreRefs: []v1alpha1.PushSecretStoreRef{
|
||||
{
|
||||
Name: PushSecretStore,
|
||||
Kind: "SecretStore",
|
||||
},
|
||||
},
|
||||
Selector: v1alpha1.PushSecretSelector{
|
||||
Secret: v1alpha1.PushSecretSecret{
|
||||
Name: SecretName,
|
||||
},
|
||||
},
|
||||
Data: []v1alpha1.PushSecretData{
|
||||
{
|
||||
Match: v1alpha1.PushSecretMatch{
|
||||
SecretKey: "key",
|
||||
RemoteRef: v1alpha1.PushSecretRemoteRef{
|
||||
RemoteKey: "path/to/key",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Template: &v1beta1.ExternalSecretTemplate{
|
||||
Metadata: v1beta1.ExternalSecretTemplateMetadata{
|
||||
Labels: map[string]string{
|
||||
"foos": "ball",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"hihi": "ga",
|
||||
},
|
||||
},
|
||||
Type: v1.SecretTypeOpaque,
|
||||
EngineVersion: v1beta1.TemplateEngineV2,
|
||||
Data: map[string]string{
|
||||
"key": "{{ .key | toString | upper }} was templated",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
|
||||
Eventually(func() bool {
|
||||
By("checking if Provider value got updated")
|
||||
providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
got := providerValue.Value
|
||||
return bytes.Equal(got, []byte("VALUE was templated"))
|
||||
}, time.Second*10, time.Second).Should(BeTrue())
|
||||
return true
|
||||
}
|
||||
}
|
||||
// if target Secret name is not specified it should use the ExternalSecret name.
|
||||
syncAndDeleteSuccessfully := func(tc *testCase) {
|
||||
fakeProvider.SetSecretFn = func() error {
|
||||
|
@ -705,6 +768,7 @@ var _ = Describe("ExternalSecret controller", func() {
|
|||
// this must be optional so we can test faulty es configuration
|
||||
},
|
||||
Entry("should sync", syncSuccessfully),
|
||||
Entry("should sync with template", syncSuccessfullyWithTemplate),
|
||||
Entry("should delete if DeletionPolicy=Delete", syncAndDeleteSuccessfully),
|
||||
Entry("should track deletion tasks if Delete fails", failDelete),
|
||||
Entry("should track deleted stores if Delete fails", failDeleteStore),
|
||||
|
|
229
pkg/controllers/templating/parser.go
Normal file
229
pkg/controllers/templating/parser.go
Normal file
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
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 templating
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||
"github.com/external-secrets/external-secrets/pkg/template"
|
||||
)
|
||||
|
||||
const fieldOwnerTemplate = "externalsecrets.external-secrets.io/%v"
|
||||
|
||||
var (
|
||||
errTplCMMissingKey = "error in configmap %s: missing key %s"
|
||||
errTplSecMissingKey = "error in secret %s: missing key %s"
|
||||
errExecTpl = "could not execute template: %w"
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
Exec template.ExecFunc
|
||||
DataMap map[string][]byte
|
||||
Client client.Client
|
||||
TargetSecret *v1.Secret
|
||||
}
|
||||
|
||||
func (p *Parser) MergeConfigMap(ctx context.Context, namespace string, tpl esv1beta1.TemplateFrom) error {
|
||||
if tpl.ConfigMap == nil {
|
||||
return nil
|
||||
}
|
||||
var cm v1.ConfigMap
|
||||
err := p.Client.Get(ctx, types.NamespacedName{
|
||||
Name: tpl.ConfigMap.Name,
|
||||
Namespace: namespace,
|
||||
}, &cm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, k := range tpl.ConfigMap.Items {
|
||||
val, ok := cm.Data[k.Key]
|
||||
out := make(map[string][]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf(errTplCMMissingKey, tpl.ConfigMap.Name, k.Key)
|
||||
}
|
||||
switch k.TemplateAs {
|
||||
case esv1beta1.TemplateScopeValues:
|
||||
out[k.Key] = []byte(val)
|
||||
case esv1beta1.TemplateScopeKeysAndValues:
|
||||
out[val] = []byte(val)
|
||||
}
|
||||
err = p.Exec(out, p.DataMap, k.TemplateAs, tpl.Target, p.TargetSecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) MergeSecret(ctx context.Context, namespace string, tpl esv1beta1.TemplateFrom) error {
|
||||
if tpl.Secret == nil {
|
||||
return nil
|
||||
}
|
||||
var sec v1.Secret
|
||||
err := p.Client.Get(ctx, types.NamespacedName{
|
||||
Name: tpl.Secret.Name,
|
||||
Namespace: namespace,
|
||||
}, &sec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, k := range tpl.Secret.Items {
|
||||
val, ok := sec.Data[k.Key]
|
||||
if !ok {
|
||||
return fmt.Errorf(errTplSecMissingKey, tpl.Secret.Name, k.Key)
|
||||
}
|
||||
out := make(map[string][]byte)
|
||||
switch k.TemplateAs {
|
||||
case esv1beta1.TemplateScopeValues:
|
||||
out[k.Key] = val
|
||||
case esv1beta1.TemplateScopeKeysAndValues:
|
||||
out[string(val)] = val
|
||||
}
|
||||
err = p.Exec(out, p.DataMap, k.TemplateAs, tpl.Target, p.TargetSecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) MergeLiteral(_ context.Context, tpl esv1beta1.TemplateFrom) error {
|
||||
if tpl.Literal == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string][]byte)
|
||||
out[*tpl.Literal] = []byte(*tpl.Literal)
|
||||
return p.Exec(out, p.DataMap, esv1beta1.TemplateScopeKeysAndValues, tpl.Target, p.TargetSecret)
|
||||
}
|
||||
|
||||
func (p *Parser) MergeTemplateFrom(ctx context.Context, namespace string, template *esv1beta1.ExternalSecretTemplate) error {
|
||||
if template == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, tpl := range template.TemplateFrom {
|
||||
err := p.MergeConfigMap(ctx, namespace, tpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.MergeSecret(ctx, namespace, tpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.MergeLiteral(ctx, tpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) MergeMap(tplMap map[string]string, target esv1beta1.TemplateTarget) error {
|
||||
byteMap := make(map[string][]byte)
|
||||
for k, v := range tplMap {
|
||||
byteMap[k] = []byte(v)
|
||||
}
|
||||
err := p.Exec(byteMap, p.DataMap, esv1beta1.TemplateScopeValues, target, p.TargetSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errExecTpl, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetManagedAnnotationKeys(secret *v1.Secret, fieldOwner string) ([]string, error) {
|
||||
return getManagedFieldKeys(secret, fieldOwner, func(fields map[string]interface{}) []string {
|
||||
metadataFields, exists := fields["f:metadata"]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
mf, ok := metadataFields.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
annotationFields, exists := mf["f:annotations"]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
af, ok := annotationFields.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var keys []string
|
||||
for k := range af {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
})
|
||||
}
|
||||
|
||||
func GetManagedLabelKeys(secret *v1.Secret, fieldOwner string) ([]string, error) {
|
||||
return getManagedFieldKeys(secret, fieldOwner, func(fields map[string]interface{}) []string {
|
||||
metadataFields, exists := fields["f:metadata"]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
mf, ok := metadataFields.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
labelFields, exists := mf["f:labels"]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
lf, ok := labelFields.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var keys []string
|
||||
for k := range lf {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
})
|
||||
}
|
||||
|
||||
func getManagedFieldKeys(
|
||||
secret *v1.Secret,
|
||||
fieldOwner string,
|
||||
process func(fields map[string]interface{}) []string,
|
||||
) ([]string, error) {
|
||||
fqdn := fmt.Sprintf(fieldOwnerTemplate, fieldOwner)
|
||||
var keys []string
|
||||
for _, v := range secret.ObjectMeta.ManagedFields {
|
||||
if v.Manager != fqdn {
|
||||
continue
|
||||
}
|
||||
fields := make(map[string]interface{})
|
||||
err := json.Unmarshal(v.FieldsV1.Raw, &fields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling managed fields: %w", err)
|
||||
}
|
||||
for _, key := range process(fields) {
|
||||
if key == "." {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, strings.TrimPrefix(key, "f:"))
|
||||
}
|
||||
}
|
||||
return keys, nil
|
||||
}
|
Loading…
Reference in a new issue