1
0
Fork 0
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:
Gergely Brautigam 2023-12-22 21:45:34 +01:00 committed by GitHub
parent 9130719b20
commit d6e24a82bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 676 additions and 188 deletions

View file

@ -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 {

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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 %}

View 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 %}

View file

@ -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
})
}

View file

@ -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) {

View 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
}

View file

@ -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),

View 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
}