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:
parent
6f36a4ceb3
commit
9f7533867d
4 changed files with 640 additions and 104 deletions
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{
|
||||
|
|
148
pkg/provider/kubernetes/metadata.go
Normal file
148
pkg/provider/kubernetes/metadata.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue