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"
|
corev1 "k8s.io/api/core/v1"
|
||||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -60,6 +62,9 @@ type PushSecretSpec struct {
|
||||||
Selector PushSecretSelector `json:"selector"`
|
Selector PushSecretSelector `json:"selector"`
|
||||||
// Secret Data that should be pushed to providers
|
// Secret Data that should be pushed to providers
|
||||||
Data []PushSecretData `json:"data,omitempty"`
|
Data []PushSecretData `json:"data,omitempty"`
|
||||||
|
// Template defines a blueprint for the created Secret resource.
|
||||||
|
// +optional
|
||||||
|
Template *esv1beta1.ExternalSecretTemplate `json:"template,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PushSecretSecret struct {
|
type PushSecretSecret struct {
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||||
metav1 "github.com/external-secrets/external-secrets/apis/meta/v1"
|
metav1 "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
@ -1195,6 +1196,11 @@ func (in *PushSecretSpec) DeepCopyInto(out *PushSecretSpec) {
|
||||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
(*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.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretSpec.
|
||||||
|
|
|
@ -162,6 +162,104 @@ spec:
|
||||||
required:
|
required:
|
||||||
- secret
|
- secret
|
||||||
type: object
|
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:
|
required:
|
||||||
- secretStoreRefs
|
- secretStoreRefs
|
||||||
- selector
|
- selector
|
||||||
|
|
|
@ -4384,6 +4384,101 @@ spec:
|
||||||
required:
|
required:
|
||||||
- secret
|
- secret
|
||||||
type: object
|
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:
|
required:
|
||||||
- secretStoreRefs
|
- secretStoreRefs
|
||||||
- selector
|
- selector
|
||||||
|
|
|
@ -3,8 +3,16 @@
|
||||||
The `PushSecret` is namespaced and it describes what data should be pushed to the secret provider.
|
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`.
|
* 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
|
``` yaml
|
||||||
{% include 'full-pushsecret.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' %}
|
{% 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
|
## Helper functions
|
||||||
|
|
||||||
!!! info inline end
|
!!! info inline end
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
{% raw %}
|
||||||
apiVersion: external-secrets.io/v1alpha1
|
apiVersion: external-secrets.io/v1alpha1
|
||||||
kind: PushSecret
|
kind: PushSecret
|
||||||
metadata:
|
metadata:
|
||||||
|
@ -12,8 +13,23 @@ spec:
|
||||||
selector:
|
selector:
|
||||||
secret:
|
secret:
|
||||||
name: pokedex-credentials # Source Kubernetes secret to be pushed
|
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:
|
data:
|
||||||
- match:
|
- match:
|
||||||
secretKey: best-pokemon # Source Kubernetes secret key to be pushed
|
secretKey: best-pokemon # Source Kubernetes secret key to be pushed
|
||||||
remoteRef:
|
remoteRef:
|
||||||
remoteKey: my-first-parameter # Remote reference (where the secret is going to be pushed)
|
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"
|
"fmt"
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
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"
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||||
// Loading registered providers.
|
"github.com/external-secrets/external-secrets/pkg/controllers/templating"
|
||||||
_ "github.com/external-secrets/external-secrets/pkg/provider/register"
|
_ "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/template"
|
||||||
"github.com/external-secrets/external-secrets/pkg/utils"
|
"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:
|
// merge template in the following order:
|
||||||
// * template.Data (highest precedence)
|
// * template.Data (highest precedence)
|
||||||
// * template.templateFrom
|
// * template.templateFrom
|
||||||
|
@ -167,14 +52,14 @@ func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1beta1.ExternalSe
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
p := Parser{
|
p := templating.Parser{
|
||||||
client: r.Client,
|
Client: r.Client,
|
||||||
targetSecret: secret,
|
TargetSecret: secret,
|
||||||
dataMap: dataMap,
|
DataMap: dataMap,
|
||||||
exec: execute,
|
Exec: execute,
|
||||||
}
|
}
|
||||||
// apply templates defined in template.templateFrom
|
// apply templates defined in template.templateFrom
|
||||||
err = p.MergeTemplateFrom(ctx, es)
|
err = p.MergeTemplateFrom(ctx, es.Namespace, es.Spec.Target.Template)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(errFetchTplFrom, err)
|
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
|
// Clean up Labels and Annotations added by the operator
|
||||||
// so that it won't leave outdated ones
|
// so that it won't leave outdated ones
|
||||||
labelKeys, err := getManagedLabelKeys(secret, es.Name)
|
labelKeys, err := templating.GetManagedLabelKeys(secret, es.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -220,7 +105,7 @@ func setMetadata(secret *v1.Secret, es *esv1beta1.ExternalSecret) error {
|
||||||
delete(secret.ObjectMeta.Labels, key)
|
delete(secret.ObjectMeta.Labels, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
annotationKeys, err := getManagedAnnotationKeys(secret, es.Name)
|
annotationKeys, err := templating.GetManagedAnnotationKeys(secret, es.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
utils.MergeStringMap(secret.ObjectMeta.Annotations, es.Spec.Target.Template.Metadata.Annotations)
|
||||||
return nil
|
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -41,16 +41,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
errFailedGetSecret = "could not get source secret"
|
errFailedGetSecret = "could not get source secret"
|
||||||
errPatchStatus = "error merging"
|
errPatchStatus = "error merging"
|
||||||
errGetSecretStore = "could not get SecretStore %q, %w"
|
errGetSecretStore = "could not get SecretStore %q, %w"
|
||||||
errGetClusterSecretStore = "could not get ClusterSecretStore %q, %w"
|
errGetClusterSecretStore = "could not get ClusterSecretStore %q, %w"
|
||||||
errGetProviderFailed = "could not start provider"
|
errSetSecretFailed = "could not write remote ref %v to target secretstore %v: %v"
|
||||||
errGetSecretsClientFailed = "could not start secrets client"
|
errFailedSetSecret = "set secret failed: %v"
|
||||||
errCloseStoreClient = "error when calling provider close method"
|
pushSecretFinalizer = "pushsecret.externalsecrets.io/finalizer"
|
||||||
errSetSecretFailed = "could not write remote ref %v to target secretstore %v: %v"
|
|
||||||
errFailedSetSecret = "set secret failed: %v"
|
|
||||||
pushSecretFinalizer = "pushsecret.externalsecrets.io/finalizer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Reconciler struct {
|
type Reconciler struct {
|
||||||
|
@ -153,6 +150,11 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
|
||||||
|
|
||||||
return ctrl.Result{}, err
|
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)
|
syncedSecrets, err := r.PushSecretToProviders(ctx, secretStores, ps, secret, mgr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, locks.ErrConflict) {
|
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
|
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.
|
// if target Secret name is not specified it should use the ExternalSecret name.
|
||||||
syncAndDeleteSuccessfully := func(tc *testCase) {
|
syncAndDeleteSuccessfully := func(tc *testCase) {
|
||||||
fakeProvider.SetSecretFn = func() error {
|
fakeProvider.SetSecretFn = func() error {
|
||||||
|
@ -705,6 +768,7 @@ var _ = Describe("ExternalSecret controller", func() {
|
||||||
// this must be optional so we can test faulty es configuration
|
// this must be optional so we can test faulty es configuration
|
||||||
},
|
},
|
||||||
Entry("should sync", syncSuccessfully),
|
Entry("should sync", syncSuccessfully),
|
||||||
|
Entry("should sync with template", syncSuccessfullyWithTemplate),
|
||||||
Entry("should delete if DeletionPolicy=Delete", syncAndDeleteSuccessfully),
|
Entry("should delete if DeletionPolicy=Delete", syncAndDeleteSuccessfully),
|
||||||
Entry("should track deletion tasks if Delete fails", failDelete),
|
Entry("should track deletion tasks if Delete fails", failDelete),
|
||||||
Entry("should track deleted stores if Delete fails", failDeleteStore),
|
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