diff --git a/lib/tbot/config/destination_kubernetes_secret.go b/lib/tbot/config/destination_kubernetes_secret.go index ebd74669da99b..01166959f87d9 100644 --- a/lib/tbot/config/destination_kubernetes_secret.go +++ b/lib/tbot/config/destination_kubernetes_secret.go @@ -43,6 +43,10 @@ type DestinationKubernetesSecret struct { // Name is the name the Kubernetes Secret that should be created and written // to. Name string `yaml:"name"` + // Labels is a set of labels to apply to the output Kubernetes secret. + // When configured, these labels will overwrite any existing labels on the + // secret. + Labels map[string]string `yaml:"labels,omitempty"` mu sync.Mutex namespace string @@ -70,6 +74,7 @@ func (dks *DestinationKubernetesSecret) secretTemplate() *corev1.Secret { ObjectMeta: v1.ObjectMeta{ Name: dks.Name, Namespace: dks.namespace, + Labels: dks.Labels, }, Data: map[string][]byte{}, } @@ -81,6 +86,12 @@ func (dks *DestinationKubernetesSecret) upsertSecret(ctx context.Context, secret WithResourceVersion(secret.ResourceVersion). WithType(secret.Type) + // If user has configured labels, we overwrite the labels on the secret. + if len(dks.Labels) > 0 { + apply = apply. + WithLabels(dks.Labels) + } + applyOpts := v1.ApplyOptions{ FieldManager: "tbot", } @@ -199,6 +210,42 @@ func (dks *DestinationKubernetesSecret) Write(ctx context.Context, name string, return trace.Wrap(err) } +// WriteMany allows you to write multiple artifacts to a destination at once. +// This should be atomic, meaning all artifacts are written or none are. Any +// artifacts that are not specified will be removed from the destination. +func (dks *DestinationKubernetesSecret) WriteMany(ctx context.Context, toWrite map[string][]byte) error { + ctx, span := tracer.Start( + ctx, + "DestinationKubernetesSecret/WriteMany", + ) + defer span.End() + + dks.mu.Lock() + defer dks.mu.Unlock() + if dks.initialized == false { + return trace.BadParameter("destination has not been initialized") + } + + secret, err := dks.getSecret(ctx) + if err != nil { + if !kubeerrors.IsNotFound(err) { + return trace.Wrap(err) + } + log.WithFields(logrus.Fields{ + "secret_name": dks.Name, + "secret_namespace": dks.namespace, + }).Warn("Kubernetes secret missing on attempt to write data. One will be created.") + // If the secret doesn't exist, we create it on write - this is ensures + // that we can recover if the secret is deleted between renewal loops. + secret = dks.secretTemplate() + } + + secret.Data = toWrite + + err = dks.upsertSecret(ctx, secret, false) + return trace.Wrap(err) +} + func (dks *DestinationKubernetesSecret) Read(ctx context.Context, name string) ([]byte, error) { ctx, span := tracer.Start( ctx, diff --git a/lib/tbot/config/destination_kubernetes_secret_test.go b/lib/tbot/config/destination_kubernetes_secret_test.go index 880868fbc09a2..f79a8301392ef 100644 --- a/lib/tbot/config/destination_kubernetes_secret_test.go +++ b/lib/tbot/config/destination_kubernetes_secret_test.go @@ -80,6 +80,17 @@ func TestDestinationKubernetesSecret(t *testing.T) { k8s: fakeClientSet(), }, }, + { + name: "labels", + dest: &DestinationKubernetesSecret{ + Name: "my-secret", + Labels: map[string]string{ + "key": "value", + "bar": "baz", + }, + k8s: fakeClientSet(), + }, + }, { name: "existing secret", dest: &DestinationKubernetesSecret{ @@ -99,6 +110,8 @@ func TestDestinationKubernetesSecret(t *testing.T) { defer cancel() require.NoError(t, tt.dest.Init(ctx, []string{})) + + // Test individual write require.NoError(t, tt.dest.Write(ctx, "artifact-a", []byte("data-a"))) require.NoError(t, tt.dest.Write(ctx, "artifact-b", []byte("data-b"))) aData, err := tt.dest.Read(ctx, "artifact-a") @@ -107,6 +120,23 @@ func TestDestinationKubernetesSecret(t *testing.T) { bData, err := tt.dest.Read(ctx, "artifact-b") require.NoError(t, err) require.Equal(t, []byte("data-b"), bData) + + // Test write many + require.NoError(t, tt.dest.WriteMany(ctx, map[string][]byte{ + "artifact-a": []byte("data-c"), + "artifact-b": []byte("data-d"), + })) + aData, err = tt.dest.Read(ctx, "artifact-a") + require.NoError(t, err) + require.Equal(t, []byte("data-c"), aData) + bData, err = tt.dest.Read(ctx, "artifact-b") + require.NoError(t, err) + require.Equal(t, []byte("data-d"), bData) + + // Check labels have been set + secret, err := tt.dest.k8s.CoreV1().Secrets("test-namespace").Get(ctx, tt.dest.Name, metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, tt.dest.Labels, secret.Labels) }) } }