1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

feat: push secret metadata (#3600)

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
This commit is contained in:
Moritz Johner 2024-10-12 20:41:10 +02:00 committed by GitHub
parent 6f36a4ceb3
commit 9f7533867d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 640 additions and 104 deletions

View file

@ -298,6 +298,74 @@ rules:
- create
```
#### PushSecret Metadata
The Kubernetes provider is able to manage both `metadata.labels` and `metadata.annotations` of the secret on the target cluster.
Users have different preferences on what metadata should be pushed. ESO by default pushes both labels and annotations to the target secret and merges them with the existing metadata.
You can specify the metadata in the `spec.template.metadata` section if you want to decouple it from the existing secret.
```yaml
{% raw %}
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
name: example
spec:
# ...
template:
metadata:
labels:
app.kubernetes.io/part-of: argocd
data:
mysql_connection_string: "mysql://{{ .hostname }}:3306/{{ .database }}"
data:
- match:
secretKey: mysql_connection_string
remoteRef:
remoteKey: backend_secrets
property: mysql_connection_string
{% endraw %}
```
Further, you can leverage the `.data[].metadata` section to fine-tine the behaviour of the metadata merge strategy. The metadata section is a versioned custom-resource _alike_ structure, the behaviour is detailed below.
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
name: example
spec:
# ...
data:
- match:
secretKey: example-1
remoteRef:
remoteKey: example-remote-secret
property: url
metadata:
apiVersion: kubernetes.external-secrets.io/v1alpha1
kind: PushSecretMetadata
spec:
sourceMergePolicy: Merge # or Replace
targetMergePolicy: Merge # or Replace / Ignore
labels:
color: red
annotations:
yes: please
```
| Field | Type | Description |
| ----------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| sourceMergePolicy | string: `Merge`, `Replace` | The sourceMergePolicy defines how the metadata of the source secret is merged. `Merge` will merge the metadata of the source secret with the metadata defined in `.data[].metadata`. With `Replace`, the metadata in `.data[].metadata` replaces the source metadata. |
| targetMergePolicy | string: `Merge`, `Replace`, `Ignore` | The targetMergePolicy defines how ESO merges the metadata produced by the sourceMergePolicy with the target secret. With `Merge`, the source metadata is merged with the existing metadata from the target secret. `Replace` will replace the target metadata with the metadata defined in the source. `Ignore` leaves the target metadata as is. |
| labels | `map[string]string` | The labels. |
| annotations | `map[string]string` | The annotations. |
#### Implementation Considerations
When utilizing the PushSecret feature and configuring the permissions for the SecretStore, consider the following:

View file

@ -15,17 +15,16 @@ limitations under the License.
package kubernetes
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"github.com/tidwall/gjson"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
@ -109,50 +108,107 @@ func (c *Client) PushSecret(ctx context.Context, secret *v1.Secret, data esv1bet
if data.GetProperty() == "" && data.GetSecretKey() != "" {
return errors.New("requires property in RemoteRef to push secret value if secret key is defined")
}
extSecret, getErr := c.userSecretClient.Get(ctx, data.GetRemoteKey(), metav1.GetOptions{})
metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesGetSecret, getErr)
if getErr != nil {
// create if it not exists
if apierrors.IsNotFound(getErr) {
typ := v1.SecretTypeOpaque
if secret.Type != "" {
typ = secret.Type
}
return c.createSecret(ctx, secret, typ, data)
}
return getErr
remoteSecret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: c.store.RemoteNamespace,
Name: data.GetRemoteKey(),
},
}
// the whole secret was pushed to the provider
if data.GetSecretKey() == "" {
if data.GetProperty() != "" {
value, err := c.marshalData(secret)
if err != nil {
return err
}
return c.createOrUpdate(ctx, remoteSecret, func() error {
return c.mergePushSecretData(data, remoteSecret, secret)
})
}
if v, ok := extSecret.Data[data.GetProperty()]; ok && bytes.Equal(v, value) {
return nil
}
func (c *Client) mergePushSecretData(remoteRef esv1beta1.PushSecretData, remoteSecret, localSecret *v1.Secret) error {
// apply secret type
secretType := v1.SecretTypeOpaque
if localSecret.Type != "" {
secretType = localSecret.Type
}
remoteSecret.Type = secretType
return c.updateProperty(ctx, extSecret, data, value)
}
if reflect.DeepEqual(extSecret.Data, secret.Data) {
return nil
}
return c.updateMap(ctx, extSecret, secret.Data)
// merge secret data with existing secret data
if remoteSecret.Data == nil {
remoteSecret.Data = make(map[string][]byte)
}
// only a single property was pushed
if v, ok := extSecret.Data[data.GetProperty()]; ok && bytes.Equal(v, secret.Data[data.GetSecretKey()]) {
pushMeta, err := parseMetadataParameters(remoteRef.GetMetadata())
if err != nil {
return fmt.Errorf("unable to parse metadata parameters: %w", err)
}
// merge metadata based on the policy
var targetLabels, targetAnnotations map[string]string
sourceLabels, sourceAnnotations, err := mergeSourceMetadata(localSecret, pushMeta)
if err != nil {
return fmt.Errorf("failed to merge source metadata: %w", err)
}
targetLabels, targetAnnotations, err = mergeTargetMetadata(remoteSecret, pushMeta, sourceLabels, sourceAnnotations)
if err != nil {
return fmt.Errorf("failed to merge target metadata: %w", err)
}
remoteSecret.ObjectMeta.Labels = targetLabels
remoteSecret.ObjectMeta.Annotations = targetAnnotations
// case 1: push the whole secret
if remoteRef.GetProperty() == "" {
for k, v := range localSecret.Data {
remoteSecret.Data[k] = v
}
return nil
}
return c.updateProperty(ctx, extSecret, data, secret.Data[data.GetSecretKey()])
// cases 2a + 2b: push into a property.
// if secret key is empty, we will marshal the whole secret and put it into
// the property defined in the remoteRef.
if remoteRef.GetSecretKey() == "" {
value, err := c.marshalData(localSecret)
if err != nil {
return err
}
remoteSecret.Data[remoteRef.GetProperty()] = value
} else {
// if secret key is defined, we will push that key from the local secret
remoteSecret.Data[remoteRef.GetProperty()] = localSecret.Data[remoteRef.GetSecretKey()]
}
return nil
}
func (c *Client) createOrUpdate(ctx context.Context, targetSecret *v1.Secret, f func() error) error {
target, err := c.userSecretClient.Get(ctx, targetSecret.Name, metav1.GetOptions{})
metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesGetSecret, err)
if err != nil {
if !apierrors.IsNotFound(err) {
return err
}
if err := f(); err != nil {
return err
}
_, err := c.userSecretClient.Create(ctx, targetSecret, metav1.CreateOptions{})
metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesCreateSecret, err)
if err != nil {
return err
}
return nil
}
*targetSecret = *target
existing := targetSecret.DeepCopyObject()
if err := f(); err != nil {
return err
}
if equality.Semantic.DeepEqual(existing, targetSecret) {
return nil
}
_, err = c.userSecretClient.Update(ctx, targetSecret, metav1.UpdateOptions{})
metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesUpdateSecret, err)
if err != nil {
return err
}
return nil
}
func (c *Client) marshalData(secret *v1.Secret) ([]byte, error) {
@ -337,41 +393,6 @@ func convertMap(in map[string][]byte) map[string]string {
return out
}
func (c *Client) createSecret(ctx context.Context, secret *v1.Secret, typed v1.SecretType, remoteRef esv1beta1.PushSecretData) error {
data := make(map[string][]byte)
if remoteRef.GetProperty() != "" {
// set a specific remote key
if remoteRef.GetSecretKey() == "" {
value, err := c.marshalData(secret)
if err != nil {
return err
}
data[remoteRef.GetProperty()] = value
} else {
// push a specific secret key into a specific remote property
data[remoteRef.GetProperty()] = secret.Data[remoteRef.GetSecretKey()]
}
} else {
// push the whole secret as is using each key of the secret as a property in the created secret
data = secret.Data
}
s := v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: remoteRef.GetRemoteKey(),
Namespace: c.store.RemoteNamespace,
},
Data: data,
Type: typed,
}
_, err := c.userSecretClient.Create(ctx, &s, metav1.CreateOptions{})
metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesCreateSecret, err)
return err
}
// fullDelete removes remote secret completely.
func (c *Client) fullDelete(ctx context.Context, secretName string) error {
err := c.userSecretClient.Delete(ctx, secretName, metav1.DeleteOptions{})
@ -392,33 +413,6 @@ func (c *Client) removeProperty(ctx context.Context, extSecret *v1.Secret, remot
return err
}
func (c *Client) updateMap(ctx context.Context, extSecret *v1.Secret, values map[string][]byte) error {
// update the existing map with values from the pushed secret but keep existing values in tack.
for k, v := range values {
extSecret.Data[k] = v
}
return c.updateSecret(ctx, extSecret)
}
func (c *Client) updateProperty(ctx context.Context, extSecret *v1.Secret, remoteRef esv1beta1.PushSecretRemoteRef, value []byte) error {
if extSecret.Data == nil {
extSecret.Data = make(map[string][]byte)
}
// otherwise update remote secret
extSecret.Data[remoteRef.GetProperty()] = value
return c.updateSecret(ctx, extSecret)
}
func (c *Client) updateSecret(ctx context.Context, extSecret *v1.Secret) error {
_, err := c.userSecretClient.Update(ctx, extSecret, metav1.UpdateOptions{})
metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesUpdateSecret, err)
return err
}
func getSecret(secret *v1.Secret, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
if ref.MetadataPolicy == esv1beta1.ExternalSecretMetadataPolicyFetch {
s, found, err := getFromSecretMetadata(secret, ref)

View file

@ -24,6 +24,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -86,8 +87,9 @@ func (fk *fakeClient) Delete(_ context.Context, name string, _ metav1.DeleteOpti
func (fk *fakeClient) Create(_ context.Context, secret *v1.Secret, _ metav1.CreateOptions) (*v1.Secret, error) {
s := &v1.Secret{
Data: secret.Data,
Type: secret.Type,
Data: secret.Data,
ObjectMeta: secret.ObjectMeta,
Type: secret.Type,
}
fk.secretMap[secret.Name] = s
return s, nil
@ -98,6 +100,7 @@ func (fk *fakeClient) Update(_ context.Context, secret *v1.Secret, _ metav1.Upda
if !ok {
return nil, errors.New("error while updating secret")
}
s.ObjectMeta = secret.ObjectMeta
s.Data = secret.Data
return s, nil
}
@ -705,6 +708,9 @@ func TestDeleteSecret(t *testing.T) {
wantErr: false,
wantSecretMap: map[string]*v1.Secret{
"mysec": {
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
},
Data: map[string][]byte{
"secret": []byte(`bar`),
},
@ -797,6 +803,11 @@ func TestPushSecret(t *testing.T) {
},
wantSecretMap: map[string]*v1.Secret{
"mysec": {
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
Labels: map[string]string{},
Annotations: map[string]string{},
},
Data: map[string][]byte{
"token": []byte(`foo`),
"token2": []byte(`foo`),
@ -827,6 +838,11 @@ func TestPushSecret(t *testing.T) {
},
wantSecretMap: map[string]*v1.Secret{
"mysec": {
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
Labels: map[string]string{},
Annotations: map[string]string{},
},
Data: map[string][]byte{
"token": []byte(`{"foo":"bar"}`),
},
@ -856,6 +872,11 @@ func TestPushSecret(t *testing.T) {
},
wantSecretMap: map[string]*v1.Secret{
"mysec": {
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
Labels: map[string]string{},
Annotations: map[string]string{},
},
Data: map[string][]byte{
"token": []byte(`foo`),
"token2": []byte(`{"foo":"bar"}`),
@ -883,6 +904,11 @@ func TestPushSecret(t *testing.T) {
},
wantSecretMap: map[string]*v1.Secret{
"mysec": {
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
Labels: map[string]string{},
Annotations: map[string]string{},
},
Data: map[string][]byte{
"marshaled": []byte(`{"token":"foo","token2":"2"}`),
},
@ -915,6 +941,11 @@ func TestPushSecret(t *testing.T) {
wantErr: false,
wantSecretMap: map[string]*v1.Secret{
"mysec": {
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
Labels: map[string]string{},
Annotations: map[string]string{},
},
Data: map[string][]byte{
"token": []byte(`foo`),
"secret": []byte(`bar`),
@ -947,6 +978,11 @@ func TestPushSecret(t *testing.T) {
wantErr: false,
wantSecretMap: map[string]*v1.Secret{
"mysec": {
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
Labels: map[string]string{},
Annotations: map[string]string{},
},
Data: map[string][]byte{
"token": []byte(`bar`),
},
@ -954,7 +990,216 @@ func TestPushSecret(t *testing.T) {
},
},
{
name: "create new secret",
name: "replace existing property in existing secret with targetMergePolicy set to Ignore",
fields: fields{
Client: &fakeClient{
t: t,
secretMap: map[string]*v1.Secret{
"mysec": {
Data: map[string][]byte{
"token": []byte(`foo`),
},
},
},
},
},
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
// these should be ignored as the targetMergePolicy is set to Ignore
Labels: map[string]string{"dev": "seb"},
Annotations: map[string]string{"date": "today"},
},
Data: map[string][]byte{secretKey: []byte("bar")},
},
data: testingfake.PushSecretData{
SecretKey: secretKey,
RemoteKey: "mysec",
Property: "token",
Metadata: &apiextensionsv1.JSON{
Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1", "kind": "PushSecretMetadata", spec: {"targetMergePolicy": "Ignore"}}`),
},
},
wantErr: false,
wantSecretMap: map[string]*v1.Secret{
"mysec": {
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
Labels: map[string]string{},
Annotations: map[string]string{},
},
Data: map[string][]byte{
"token": []byte(`bar`),
},
},
},
},
{
name: "replace existing property in existing secret with targetMergePolicy set to Replace",
fields: fields{
Client: &fakeClient{
t: t,
secretMap: map[string]*v1.Secret{
"mysec": {
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
Labels: map[string]string{
"already": "existing",
},
Annotations: map[string]string{
"already": "existing",
},
},
Data: map[string][]byte{
"token": []byte(`foo`),
},
},
},
},
},
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
// these should replace existing metadata as the targetMergePolicy is set to Replace
Labels: map[string]string{"dev": "seb"},
Annotations: map[string]string{"date": "today"},
},
Data: map[string][]byte{secretKey: []byte("bar")},
},
data: testingfake.PushSecretData{
SecretKey: secretKey,
RemoteKey: "mysec",
Property: "token",
Metadata: &apiextensionsv1.JSON{
Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1", "kind": "PushSecretMetadata", spec: {"targetMergePolicy": "Replace"}}`),
},
},
wantErr: false,
wantSecretMap: map[string]*v1.Secret{
"mysec": {
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
Labels: map[string]string{
"dev": "seb",
},
Annotations: map[string]string{
"date": "today",
},
},
Data: map[string][]byte{
"token": []byte(`bar`),
},
},
},
},
{
name: "create new secret, merging existing metadata",
fields: fields{
Client: &fakeClient{
t: t,
secretMap: map[string]*v1.Secret{
"yoursec": {
Data: map[string][]byte{
"token": []byte(`foo`),
},
},
},
},
},
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"this-annotation": "should be present on the targey secret",
},
},
Data: map[string][]byte{secretKey: []byte("bar")},
},
data: testingfake.PushSecretData{
SecretKey: secretKey,
RemoteKey: "mysec",
Property: "secret",
Metadata: &apiextensionsv1.JSON{
Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1", "kind": "PushSecretMetadata", spec: {"annotations": {"date": "today"}, "labels": {"dev": "seb"}}}`),
},
},
wantErr: false,
wantSecretMap: map[string]*v1.Secret{
"yoursec": {
Data: map[string][]byte{
"token": []byte(`foo`),
},
},
"mysec": {
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
Annotations: map[string]string{
"date": "today",
"this-annotation": "should be present on the targey secret",
},
Labels: map[string]string{"dev": "seb"},
},
Data: map[string][]byte{
"secret": []byte(`bar`),
},
Type: v1.SecretTypeOpaque,
},
},
},
{
name: "create new secret with metadata from secret metadata and remoteRef.metadata",
fields: fields{
Client: &fakeClient{
t: t,
secretMap: map[string]*v1.Secret{
"yoursec": {
Data: map[string][]byte{
"token": []byte(`foo`),
},
},
},
},
},
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{"date": "today"},
Labels: map[string]string{"dev": "seb"},
},
Data: map[string][]byte{secretKey: []byte("bar")},
},
data: testingfake.PushSecretData{
SecretKey: secretKey,
RemoteKey: "mysec",
Property: "secret",
Metadata: &apiextensionsv1.JSON{
Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1", "kind": "PushSecretMetadata", spec: { "sourceMergePolicy": "Replace", "annotations": {"another-field": "from-remote-ref"}, "labels": {"other-label": "from-remote-ref"}}}`),
},
},
wantErr: false,
wantSecretMap: map[string]*v1.Secret{
"yoursec": {
Data: map[string][]byte{
"token": []byte(`foo`),
},
},
"mysec": {
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
Annotations: map[string]string{
"another-field": "from-remote-ref",
},
Labels: map[string]string{
"other-label": "from-remote-ref",
},
},
Data: map[string][]byte{
"secret": []byte(`bar`),
},
Type: v1.SecretTypeOpaque,
},
},
},
{
name: "invalid secret metadata structure results in error",
fields: fields{
Client: &fakeClient{
t: t,
@ -974,6 +1219,75 @@ func TestPushSecret(t *testing.T) {
SecretKey: secretKey,
RemoteKey: "mysec",
Property: "secret",
Metadata: &apiextensionsv1.JSON{
Raw: []byte(`{}`),
},
},
wantErr: true,
wantSecretMap: map[string]*v1.Secret{
"yoursec": {
Data: map[string][]byte{
"token": []byte(`foo`),
},
},
},
},
{
name: "non-json secret metadata results in error",
fields: fields{
Client: &fakeClient{
t: t,
secretMap: map[string]*v1.Secret{
"yoursec": {
Data: map[string][]byte{
"token": []byte(`foo`),
},
},
},
},
},
secret: &v1.Secret{
Data: map[string][]byte{secretKey: []byte("bar")},
},
data: testingfake.PushSecretData{
SecretKey: secretKey,
RemoteKey: "mysec",
Property: "secret",
Metadata: &apiextensionsv1.JSON{
Raw: []byte(`--- not json ---`),
},
},
wantErr: true,
wantSecretMap: map[string]*v1.Secret{
"yoursec": {
Data: map[string][]byte{
"token": []byte(`foo`),
},
},
},
},
{
name: "create new secret with whole secret",
fields: fields{
Client: &fakeClient{
t: t,
secretMap: map[string]*v1.Secret{
"yoursec": {
Data: map[string][]byte{
"token": []byte(`foo`),
},
},
},
},
},
secret: &v1.Secret{
Data: map[string][]byte{
"foo": []byte("bar"),
"baz": []byte("bang"),
},
},
data: testingfake.PushSecretData{
RemoteKey: "mysec",
},
wantErr: false,
wantSecretMap: map[string]*v1.Secret{
@ -983,8 +1297,14 @@ func TestPushSecret(t *testing.T) {
},
},
"mysec": {
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
Labels: map[string]string{},
Annotations: map[string]string{},
},
Data: map[string][]byte{
"secret": []byte(`bar`),
"foo": []byte("bar"),
"baz": []byte("bang"),
},
Type: v1.SecretTypeOpaque,
},
@ -1021,13 +1341,19 @@ func TestPushSecret(t *testing.T) {
},
},
"mysec": {
ObjectMeta: metav1.ObjectMeta{
Name: "mysec",
Labels: map[string]string{},
Annotations: map[string]string{},
},
Data: map[string][]byte{
"config.json": []byte(`{"auths": {"myregistry.localhost": {"username": "{{ .username }}", "password": "{{ .password }}"}}}`),
},
Type: v1.SecretTypeDockerConfigJson,
},
},
}}
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Client{

View file

@ -0,0 +1,148 @@
/*
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 kubernetes
import (
"fmt"
v1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
)
const (
metadataAPIVersion = "kubernetes.external-secrets.io/v1alpha1"
metadataKind = "PushSecretMetadata"
)
type PushSecretMetadata struct {
metav1.TypeMeta
Spec PushSecretMetadataSpec `json:"spec,omitempty"`
}
type PushSecretMetadataSpec struct {
TargetMergePolicy targetMergePolicy `json:"targetMergePolicy,omitempty"`
SourceMergePolicy sourceMergePolicy `json:"sourceMergePolicy,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
}
type targetMergePolicy string
const (
targetMergePolicyMerge targetMergePolicy = "Merge"
targetMergePolicyReplace targetMergePolicy = "Replace"
targetMergePolicyIgnore targetMergePolicy = "Ignore"
)
type sourceMergePolicy string
const (
sourceMergePolicyMerge sourceMergePolicy = "Merge"
sourceMergePolicyReplace sourceMergePolicy = "Replace"
)
func parseMetadataParameters(data *apiextensionsv1.JSON) (*PushSecretMetadata, error) {
if data == nil {
return nil, nil
}
var metadata PushSecretMetadata
err := yaml.Unmarshal(data.Raw, &metadata, yaml.DisallowUnknownFields)
if err != nil {
return nil, fmt.Errorf("failed to parse %s %s: %w", metadataAPIVersion, metadataKind, err)
}
if metadata.APIVersion != metadataAPIVersion {
return nil, fmt.Errorf("unexpected apiVersion %q, expected %q", metadata.APIVersion, metadataAPIVersion)
}
if metadata.Kind != metadataKind {
return nil, fmt.Errorf("unexpected kind %q, expected %q", metadata.Kind, metadataKind)
}
return &metadata, nil
}
// Takes the local secret metadata and merges it with the push metadata.
// The push metadata takes precedence.
// Depending on the policy, we either merge or overwrite the metadata from the local secret.
func mergeSourceMetadata(localSecret *v1.Secret, pushMeta *PushSecretMetadata) (map[string]string, map[string]string, error) {
labels := localSecret.ObjectMeta.Labels
annotations := localSecret.ObjectMeta.Annotations
if pushMeta == nil {
return labels, annotations, nil
}
if labels == nil {
labels = make(map[string]string)
}
if annotations == nil {
annotations = make(map[string]string)
}
switch pushMeta.Spec.SourceMergePolicy {
case "", sourceMergePolicyMerge:
for k, v := range pushMeta.Spec.Labels {
labels[k] = v
}
for k, v := range pushMeta.Spec.Annotations {
annotations[k] = v
}
case sourceMergePolicyReplace:
labels = pushMeta.Spec.Labels
annotations = pushMeta.Spec.Annotations
default:
return nil, nil, fmt.Errorf("unexpected source merge policy %q", pushMeta.Spec.SourceMergePolicy)
}
return labels, annotations, nil
}
// Takes the remote secret metadata and merges it with the source metadata.
// The source metadata may replace the existing labels/annotations
// or merge into it depending on policy.
func mergeTargetMetadata(remoteSecret *v1.Secret, pushMeta *PushSecretMetadata, sourceLabels, sourceAnnotations map[string]string) (map[string]string, map[string]string, error) {
labels := remoteSecret.ObjectMeta.Labels
annotations := remoteSecret.ObjectMeta.Annotations
if labels == nil {
labels = make(map[string]string)
}
if annotations == nil {
annotations = make(map[string]string)
}
var targetMergePolicy targetMergePolicy
if pushMeta != nil {
targetMergePolicy = pushMeta.Spec.TargetMergePolicy
}
switch targetMergePolicy {
case "", targetMergePolicyMerge:
for k, v := range sourceLabels {
labels[k] = v
}
for k, v := range sourceAnnotations {
annotations[k] = v
}
case targetMergePolicyReplace:
labels = sourceLabels
annotations = sourceAnnotations
case targetMergePolicyIgnore:
// leave the target metadata as is
// this is useful when we only want to push data
// and the user does not want to touch the metadata
default:
return nil, nil, fmt.Errorf("unexpected target merge policy %q", targetMergePolicy)
}
return labels, annotations, nil
}