Skip to content

Commit

Permalink
Add support for customizing secret labels and annotations (#56)
Browse files Browse the repository at this point in the history
Resolves #46
  • Loading branch information
mumoshu committed Jun 13, 2022
1 parent b279f8d commit 0064342
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 23 deletions.
10 changes: 10 additions & 0 deletions README.md
Expand Up @@ -140,6 +140,16 @@ In case of plain secret format, the whole content of the secret is exposed as a
}
```

## Advanced Configuration

The following spec fields are defined to customize the generated Secret:

- `spec.type` maps to generated secret's `type` field
- `spec.annotations` maps to generated secret's `metadata.annotations` field
- `spec.labels` maps to genrated secret's `metadata.labels` field

Note that `AWSSecret`'s `metadata.annotations` and `metadata.labels` are not propagated down to the generate secret. Use `spec.annotations` and `spec.labels` instead.

## Installation

```bash
Expand Down
10 changes: 9 additions & 1 deletion api/mumoshu/v1alpha1/awssecret_types.go
Expand Up @@ -11,7 +11,7 @@ import (
// AWSSecretSpec defines the desired state of AWSSecret
type AWSSecretSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
// Important: Run "make generate" to regenerate code after modifying this file

// DataFrom data field is used to store arbitrary data, encoded using base64.
DataFrom DataFrom `json:"dataFrom,omitempty"`
Expand All @@ -23,6 +23,14 @@ type AWSSecretSpec struct {
// Used to facilitate programmatic handling of secret data.
// +optional
Type corev1.SecretType `json:"type,omitempty"`

// +optional
Metadata *SecretMeta `json:"metadata,omitempty"`
}

type SecretMeta struct {
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
}

// StringDataFrom defines how the resulting Secret's `stringData` is built
Expand Down
36 changes: 35 additions & 1 deletion api/mumoshu/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 46 additions & 15 deletions controllers/awssecret_controller.go
Expand Up @@ -2,8 +2,10 @@ package controllers

import (
"context"
"reflect"
"time"

"github.com/go-logr/logr"
mumoshuv1alpha1 "github.com/mumoshu/aws-secret-operator/api/mumoshu/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -19,8 +21,6 @@ import (
errs "github.com/pkg/errors"
)

var log = logf.Log.WithName("controller_awssecret")

func (r *AWSSecretController) SetupWithManager(mgr ctrl.Manager) error {
var name = "awssecret-controller"

Expand All @@ -47,6 +47,7 @@ type AWSSecretController struct {
Scheme *runtime.Scheme

SyncContext *SyncContext
Log *logr.Logger
}

// Reconcile reads that state of the cluster for a AWSSecret object and makes changes based on the state read
Expand All @@ -55,7 +56,14 @@ type AWSSecretController struct {
// The Controller will requeue the Request to be processed again if the returned error is non-nil or
// Result.Requeue is true, otherwise upon completion it will .
func (r *AWSSecretController) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
var log logr.Logger
if r.Log != nil {
log = *r.Log
} else {
log = logf.Log
}

reqLogger := log.WithName("controller_awssecret").WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)

// Fetch the AWSSecret instance
instance := &mumoshuv1alpha1.AWSSecret{}
Expand All @@ -72,7 +80,7 @@ func (r *AWSSecretController) Reconcile(ctx context.Context, request reconcile.R
}

// Define a new Secret object
desired, err := r.newSecretForCR(instance)
desired, err := r.newSecretForCR(reqLogger, instance)
if err != nil {
return reconcile.Result{}, errs.Wrap(err, "failed to compute secret for cr")
}
Expand All @@ -99,9 +107,23 @@ func (r *AWSSecretController) Reconcile(ctx context.Context, request reconcile.R
return reconcile.Result{}, err
}

// if Secret exists, only update if versionId has changed
var changed []string

if string(current.Data["AWSVersionId"]) != desired.StringData["AWSVersionId"] {
reqLogger.Info("versionId changed, Updating the Secret", "desired.Namespace", desired.Namespace, "desired.Name", desired.Name)
changed = append(changed, "versionId")
}

if !reflect.DeepEqual(current.Labels, desired.Labels) {
changed = append(changed, "labels")
}

if !reflect.DeepEqual(current.Annotations, desired.Annotations) {
changed = append(changed, "annotations")
}

// if Secret exists, only update if versionId has changed
if len(changed) > 0 {
reqLogger.Info("Detected changes. Updating the Secret", "changed", changed, "desired.Namespace", desired.Namespace, "desired.Name", desired.Name)
err = r.Client.Update(ctx, desired)
if err != nil {
return reconcile.Result{}, err
Expand All @@ -115,10 +137,7 @@ func (r *AWSSecretController) Reconcile(ctx context.Context, request reconcile.R
}

// newSecretForCR returns a Secret with the name/namespace defined in the cr
func (r *AWSSecretController) newSecretForCR(cr *mumoshuv1alpha1.AWSSecret) (*corev1.Secret, error) {
labels := map[string]string{
"app": cr.Name,
}
func (r *AWSSecretController) newSecretForCR(reqLogger logr.Logger, cr *mumoshuv1alpha1.AWSSecret) (*corev1.Secret, error) {
if r.SyncContext == nil {
r.SyncContext = newContext(nil)
}
Expand All @@ -144,15 +163,27 @@ func (r *AWSSecretController) newSecretForCR(cr *mumoshuv1alpha1.AWSSecret) (*co
}
}

return &corev1.Secret{
var labels, annotations map[string]string
if m := cr.Spec.Metadata; m != nil {
labels, annotations = m.Labels, m.Annotations
}

secret := &corev1.Secret{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: cr.Name,
Namespace: cr.Namespace,
Labels: labels,
Name: cr.Name,
Namespace: cr.Namespace,
Labels: labels,
Annotations: annotations,
},
Data: data,
StringData: stringData,
Type: cr.Spec.Type,
}, nil
}

if reqLogger.V(2).Enabled() {
reqLogger.V(2).Info("Dumping the desired secret", "meta", secret.ObjectMeta, "stringData", secret.StringData)
}

return secret, nil
}
15 changes: 15 additions & 0 deletions controllers/awssecret_controller_test.go
Expand Up @@ -4,10 +4,14 @@ import (
"context"
"fmt"
"math/rand"
"time"

zaplib "go.uber.org/zap"
"go.uber.org/zap/zapcore"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -42,8 +46,18 @@ func SetupIntegrationTest(ctx2 context.Context) *testEnvironment {
err := k8sClient.Create(ctx, ns)
Expect(err).NotTo(HaveOccurred(), "failed to create test namespace")

logger := zap.New(func(o *zap.Options) {
// For example, --log-level=debug a.k.a --log-level=-1 maps to zaplib.DebugLevel, which is associated to logr's V(1)
// --log-level=-2 maps the specific custom log level that is associated to logr's V(2).
level := zapcore.Level(-2)
atomicLevel := zaplib.NewAtomicLevelAt(level)
o.Level = &atomicLevel
o.TimeEncoder = zapcore.TimeEncoderOfLayout(time.RFC3339)
})

mgr, err := ctrl.NewManager(cfg, ctrl.Options{
Namespace: ns.Name,
Logger: logger,
})
Expect(err).NotTo(HaveOccurred(), "failed to create manager")

Expand All @@ -55,6 +69,7 @@ func SetupIntegrationTest(ctx2 context.Context) *testEnvironment {
Name: controllerName("awssecret"),
Client: mgr.GetClient(),
Scheme: scheme.Scheme,
Log: &logger,
}
err = awsSecretController.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup runner controller")
Expand Down
66 changes: 60 additions & 6 deletions controllers/tester.go
Expand Up @@ -22,8 +22,8 @@ import (
)

var (
retryInterval = time.Second * 5
timeout = time.Second * 60
retryInterval = time.Second * 1
timeout = time.Second * 3
)

// This runs a series of AWS API calls and K8s API calls
Expand Down Expand Up @@ -119,7 +119,7 @@ func awsSecretTest(ctx context.Context, client client.Client, namespace string)
return err
}

err = waitForSecret(ctx, log, client, namespace, "example-secret", map[string]string{"value": "v1value"}, retryInterval, timeout)
err = waitForSecret(ctx, log, client, "creation", namespace, "example-secret", map[string]string{"value": "v1value"}, nil, nil, retryInterval, timeout)
if err != nil {
return err
}
Expand All @@ -128,18 +128,50 @@ func awsSecretTest(ctx context.Context, client client.Client, namespace string)
if err != nil {
return err
}

exampleAWSSecret.Spec.StringDataFrom.SecretsManagerSecretRef.VersionId = versionIDV2
err = client.Update(ctx, exampleAWSSecret)
if err != nil {
return err
}

// wait for example-secret to be updated
return waitForSecret(ctx, log, client, namespace, "example-secret", map[string]string{"value": "v2value"}, retryInterval, timeout)
err = waitForSecret(ctx, log, client, "update", namespace, "example-secret", map[string]string{"value": "v2value"}, nil, nil, retryInterval, timeout)
if err != nil {
return err
}

err = client.Get(ctx, types.NamespacedName{Name: "example-secret", Namespace: namespace}, exampleAWSSecret)
if err != nil {
return err
}

labels := map[string]string{"label1": "labelv1"}
annotations := map[string]string{"annotation1": "annotationv1"}

exampleAWSSecret.Spec.Metadata = &operator.SecretMeta{
Labels: labels,
Annotations: annotations,
}
err = client.Update(ctx, exampleAWSSecret)
if err != nil {
return err
}

log.Info("Updated awsSecret", "spec.metadata", exampleAWSSecret.Spec.Metadata)

// wait for example-secret to have the custom label and annotation
err = waitForSecret(ctx, log, client, "custom label and annotation", namespace, "example-secret", map[string]string{"value": "v2value"}, labels, annotations, retryInterval, timeout)
if err != nil {
return err
}

return nil
}

func waitForSecret(ctx context.Context, log logr.Logger, client client.Client, namespace, name string,
func waitForSecret(ctx context.Context, log logr.Logger, client client.Client, desc, namespace, name string,
expectedKVs map[string]string,
labels, annotations map[string]string,
retryInterval, timeout time.Duration) error {

err := wait.Poll(retryInterval, timeout, func() (done bool, err error) {
Expand All @@ -164,10 +196,32 @@ func waitForSecret(ctx context.Context, log logr.Logger, client client.Client, n
}
}

if want, got := len(labels), len(secret.Labels); want != got {
log.Info("Still waiting for labels to be updated", "want", want, "got", got, "observed", secret.Labels)
return false, nil
}
for k, want := range labels {
if got := secret.Labels[k]; want != got {
log.Info("Still waiting for label to be updated", "label", k, "want", want, "got", got)
return false, nil
}
}

if want, got := len(annotations), len(secret.Annotations); want != got {
log.Info("Still waiting for annotations to be updated", "key", want, "got", got)
return false, nil
}
for k, want := range annotations {
if got := secret.Annotations[k]; want != got {
log.Info("Still waiting for annotation to be updated", "key", k, "want", want, "got", got)
return false, nil
}
}

return true, nil
})
if err != nil {
return err
return fmt.Errorf("failed while waiting for %s: %w", desc, err)
}
log.Info("Secret available", "name", name)
return nil
Expand Down
11 changes: 11 additions & 0 deletions deploy/crds/mumoshu.github.io_awssecrets.yaml
Expand Up @@ -55,6 +55,17 @@ spec:
type: string
type: object
type: object
metadata:
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
stringDataFrom:
description: StringDataFrom stringData field is provided for convenience,
and allows you to provide secret data as unencoded strings.
Expand Down

0 comments on commit 0064342

Please sign in to comment.