Skip to content

Commit fa2a8c9

Browse files
committed
support additional kubernetes secret labels
1 parent ebfbe1b commit fa2a8c9

File tree

4 files changed

+237
-12
lines changed

4 files changed

+237
-12
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,26 @@ mappings:
2626
secretName: k8s-secretname
2727
vaultEngineType: # optionally "kv" or "kv-v2" to override the defaultEngineType specified above
2828
secretType: Opaque # optionally - default "Opaque" e.g.: "kubernetes.io/tls"
29+
additionalSecretLabels: # optionally add labels to the secret
30+
environment: dev
31+
team: core-services
2932
# mappings from google secrets manager paths to kubernetes secret names
3033
- sourceType: gsm
3134
path: projects/my-project/secrets/my-secret/versions/latest
3235
secretName: my-secret
3336
- sourceType: gsm
3437
path: projects/my-project/secrets/my-other-secret
3538
secretName: defaults-to-latest-version
39+
additionalSecretLabels:
40+
environment: dev
41+
team: core-services
3642
```
3743
3844
### Labels and Reconciliation
3945
By default, Pentagon will add a [metadata label](https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#ObjectMeta) with the key `pentagon` and the value `default`. At the least, this helps identify Pentagon as the creator and maintainer of the secret.
4046

47+
You can also specify custom labels for each secret mapping using the `additionalSecretLabels` filed. These labels will be added to the Kubernetes secret alongside the required `pentagon` label.
48+
4149
If you set the `label` configuration parameter, you can control the value of the label, allowing multiple Pentagon instances to exist without stepping on each other. Setting a non-default `label` also enables reconciliation which will cleanup any secrets that were created by Pentagon with a matching label, but are no longer present in the `mappings` configuration. This provides a simple way to ensure that old secret data does not remain present in your system after its time has passed.
4250

4351
### About Vault Engine Types
@@ -202,4 +210,4 @@ subjects:
202210
Pentagon is a production of Vimeo's Core Services team with lots of support from Vimeo SRE.
203211
* [@sergiosalvatore](https://github.com/sergiosalvatore)
204212
* [@dfinkel](https://github.com/dfinkel)
205-
* [@sachinagada](https://github.com/sachinagada)
213+
* [@sachinagada](https://github.com/sachinagada)

config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,8 @@ type Mapping struct {
191191
// use for this secret's value in cases where gsmEncodingType is *not* json. If
192192
// this is unset, the key name will default to the value of secretName.
193193
GSMSecretKeyValue string `yaml:"gsmSecretKeyValue"`
194+
195+
// AdditionalSecretLabels allows you to specify the additional labels that will be
196+
// added to the created Kubernetes secret.
197+
AdditionalSecretLabels map[string]string `yaml:"additionalSecretLabels"`
194198
}

reflector.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"log"
8+
"maps"
89

910
"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
1011
corev1 "k8s.io/api/core/v1"
@@ -186,11 +187,18 @@ func (r *Reflector) getGSMSecret(ctx context.Context, mapping Mapping) (map[stri
186187
}
187188

188189
func (r *Reflector) createK8sSecret(ctx context.Context, mapping Mapping, data map[string][]byte) error {
190+
labels := make(map[string]string)
191+
if mapping.AdditionalSecretLabels != nil {
192+
labels = maps.Clone(mapping.AdditionalSecretLabels)
193+
}
194+
195+
labels[LabelKey] = r.labelValue
196+
189197
secret := &corev1.Secret{
190198
ObjectMeta: metav1.ObjectMeta{
191199
Name: mapping.SecretName,
192200
Namespace: r.k8sNamespace,
193-
Labels: map[string]string{LabelKey: r.labelValue},
201+
Labels: labels,
194202
},
195203
Data: data,
196204
Type: mapping.SecretType,

reflector_test.go

Lines changed: 215 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"encoding/json"
66
"testing"
77

8+
"maps"
9+
810
v1 "k8s.io/api/core/v1"
911
"k8s.io/apimachinery/pkg/api/errors"
1012
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -67,11 +69,10 @@ func TestReflectorSimple(t *testing.T) {
6769
t.Fatalf("secret should be there: %s", err)
6870
}
6971

70-
if secret.Labels[LabelKey] != DefaultLabelValue {
71-
t.Fatalf(
72-
"secret pentagon label should be %s is %s",
73-
DefaultLabelValue,
74-
secret.Labels[LabelKey],
72+
// no additional labels provided. check for default
73+
if !maps.Equal(secret.Labels, map[string]string{LabelKey: DefaultLabelValue}) {
74+
t.Fatalf("labels do not match: got %v, want %v", secret.Labels,
75+
map[string]string{LabelKey: DefaultLabelValue},
7576
)
7677
}
7778

@@ -85,6 +86,116 @@ func TestReflectorSimple(t *testing.T) {
8586
})
8687
}
8788

89+
func TestReflectorAdditionalSecretLabelsVault(t *testing.T) {
90+
allEngineTest(t, func(t testing.TB, engineType vault.EngineType) {
91+
ctx := context.Background()
92+
93+
k8sClient := k8sfake.NewSimpleClientset()
94+
vaultClient := vault.NewMock(map[string]vault.EngineType{
95+
"secrets": engineType,
96+
})
97+
98+
data := map[string]interface{}{
99+
"foo": "bar",
100+
"bar": "baz",
101+
}
102+
vaultClient.Write("secrets/data/foo", data)
103+
104+
r := NewReflector(
105+
vaultClient,
106+
gsm.NewMockGSM(nil),
107+
k8sClient, DefaultNamespace,
108+
DefaultLabelValue,
109+
)
110+
111+
err := r.Reflect(ctx, []Mapping{
112+
{
113+
SourceType: "vault",
114+
Path: "secrets/data/foo",
115+
SecretName: "foo",
116+
VaultEngineType: engineType,
117+
AdditionalSecretLabels: map[string]string{"secret": "foo"},
118+
},
119+
})
120+
if err != nil {
121+
t.Fatalf("reflect didn't work: %s", err)
122+
}
123+
124+
// now get the secret out of k8s
125+
secrets := k8sClient.CoreV1().Secrets(DefaultNamespace)
126+
127+
secret, err := secrets.Get(ctx, "foo", metav1.GetOptions{})
128+
if err != nil {
129+
t.Fatalf("secret should be there: %s", err)
130+
}
131+
132+
// check additional labels and default
133+
if !maps.Equal(secret.Labels, map[string]string{LabelKey: DefaultLabelValue, "secret": "foo"}) {
134+
t.Fatalf("labels do not match: got %v, want %v", secret.Labels,
135+
map[string]string{LabelKey: DefaultLabelValue, "secret": "foo"},
136+
)
137+
}
138+
139+
if string(secret.Data["foo"]) != "bar" {
140+
t.Fatalf("secret value does not equal bar: %s", string(secret.Data["foo"]))
141+
}
142+
143+
if string(secret.Data["bar"]) != "baz" {
144+
t.Fatalf("secret value does not equal baz: %s", string(secret.Data["bar"]))
145+
}
146+
})
147+
}
148+
149+
func TestReflectorDefaultLabelOverwriteVault(t *testing.T) {
150+
allEngineTest(t, func(t testing.TB, engineType vault.EngineType) {
151+
ctx := context.Background()
152+
153+
k8sClient := k8sfake.NewSimpleClientset()
154+
vaultClient := vault.NewMock(map[string]vault.EngineType{
155+
"secrets": engineType,
156+
})
157+
158+
data := map[string]interface{}{
159+
"foo": "bar",
160+
"bar": "baz",
161+
}
162+
vaultClient.Write("secrets/data/foo", data)
163+
164+
r := NewReflector(
165+
vaultClient,
166+
gsm.NewMockGSM(nil),
167+
k8sClient, DefaultNamespace,
168+
DefaultLabelValue,
169+
)
170+
171+
err := r.Reflect(ctx, []Mapping{
172+
{
173+
SourceType: "vault",
174+
Path: "secrets/data/foo",
175+
SecretName: "foo",
176+
VaultEngineType: engineType,
177+
AdditionalSecretLabels: map[string]string{LabelKey: "wrong-value"},
178+
},
179+
})
180+
if err != nil {
181+
t.Fatalf("reflect didn't work: %s", err)
182+
}
183+
184+
// now get the secret out of k8s
185+
secrets := k8sClient.CoreV1().Secrets(DefaultNamespace)
186+
187+
secret, err := secrets.Get(ctx, "foo", metav1.GetOptions{})
188+
if err != nil {
189+
t.Fatalf("secret should be there: %s", err)
190+
}
191+
192+
// ensure default `pentagon` label was not overwritten
193+
if string(secret.Labels[LabelKey]) != DefaultLabelValue {
194+
t.Fatalf("default pentagon label should be %s is %s", DefaultLabelValue, secret.Labels[LabelKey])
195+
}
196+
})
197+
}
198+
88199
func TestReflectorGSM(t *testing.T) {
89200
ctx := context.Background()
90201
k8sClient := k8sfake.NewSimpleClientset()
@@ -120,19 +231,113 @@ func TestReflectorGSM(t *testing.T) {
120231
t.Fatalf("secret should be there: %s", err)
121232
}
122233

123-
if secret.Labels[LabelKey] != DefaultLabelValue {
124-
t.Fatalf(
125-
"secret pentagon label should be %s is %s",
126-
DefaultLabelValue,
127-
secret.Labels[LabelKey],
234+
// no additional labels provided. check for default
235+
if !maps.Equal(secret.Labels, map[string]string{LabelKey: DefaultLabelValue}) {
236+
t.Fatalf("labels do not match: got %v, want %v", secret.Labels,
237+
map[string]string{LabelKey: DefaultLabelValue},
238+
)
239+
}
240+
241+
if string(secret.Data["foo-key"]) != "foo_bar_latest" {
242+
t.Fatalf("secret value does not equal foo_bar_latest: %s", string(secret.Data["foo"]))
243+
}
244+
}
245+
246+
func TestReflectorAdditionalSecretLabelsGSM(t *testing.T) {
247+
ctx := context.Background()
248+
k8sClient := k8sfake.NewSimpleClientset()
249+
250+
gsm := gsm.NewMockGSM(map[string][]byte{
251+
"projects/foo/secrets/bar/versions/latest": []byte("foo_bar_latest"),
252+
})
253+
254+
r := NewReflector(
255+
nil,
256+
gsm,
257+
k8sClient, DefaultNamespace,
258+
DefaultLabelValue,
259+
)
260+
261+
err := r.Reflect(ctx, []Mapping{
262+
{
263+
SourceType: "gsm",
264+
Path: "projects/foo/secrets/bar/versions/latest",
265+
SecretName: "foo",
266+
GSMSecretKeyValue: "foo-key",
267+
AdditionalSecretLabels: map[string]string{"secret": "foo"},
268+
},
269+
})
270+
if err != nil {
271+
t.Fatalf("reflect didn't work: %s", err)
272+
}
273+
274+
// now get the secret out of k8s
275+
secrets := k8sClient.CoreV1().Secrets(DefaultNamespace)
276+
277+
secret, err := secrets.Get(ctx, "foo", metav1.GetOptions{})
278+
if err != nil {
279+
t.Fatalf("secret should be there: %s", err)
280+
}
281+
282+
// check additional labels and default
283+
if !maps.Equal(secret.Labels, map[string]string{LabelKey: DefaultLabelValue, "secret": "foo"}) {
284+
t.Fatalf("labels do not match: got %v, want %v", secret.Labels,
285+
map[string]string{LabelKey: DefaultLabelValue, "secret": "foo"},
128286
)
129287
}
130288

289+
// ensure default `pentagon` label was no overwritten
290+
if string(secret.Labels[LabelKey]) != DefaultLabelValue {
291+
t.Fatalf("default pentagon label should be %s is %s", DefaultLabelValue, secret.Labels[LabelKey])
292+
}
293+
131294
if string(secret.Data["foo-key"]) != "foo_bar_latest" {
132295
t.Fatalf("secret value does not equal foo_bar_latest: %s", string(secret.Data["foo"]))
133296
}
134297
}
135298

299+
func TestReflectorDefaultLabelOverwriteGSM(t *testing.T) {
300+
ctx := context.Background()
301+
k8sClient := k8sfake.NewSimpleClientset()
302+
303+
gsm := gsm.NewMockGSM(map[string][]byte{
304+
"projects/foo/secrets/bar/versions/latest": []byte("foo_bar_latest"),
305+
})
306+
307+
r := NewReflector(
308+
nil,
309+
gsm,
310+
k8sClient, DefaultNamespace,
311+
DefaultLabelValue,
312+
)
313+
314+
err := r.Reflect(ctx, []Mapping{
315+
{
316+
SourceType: "gsm",
317+
Path: "projects/foo/secrets/bar/versions/latest",
318+
SecretName: "foo",
319+
GSMSecretKeyValue: "foo-key",
320+
AdditionalSecretLabels: map[string]string{LabelKey: "wrong-value"},
321+
},
322+
})
323+
if err != nil {
324+
t.Fatalf("reflect didn't work: %s", err)
325+
}
326+
327+
// now get the secret out of k8s
328+
secrets := k8sClient.CoreV1().Secrets(DefaultNamespace)
329+
330+
secret, err := secrets.Get(ctx, "foo", metav1.GetOptions{})
331+
if err != nil {
332+
t.Fatalf("secret should be there: %s", err)
333+
}
334+
335+
// ensure default `pentagon` label was not overwritten
336+
if string(secret.Labels[LabelKey]) != DefaultLabelValue {
337+
t.Fatalf("default pentagon label should be %s is %s", DefaultLabelValue, secret.Labels[LabelKey])
338+
}
339+
}
340+
136341
func TestReflectorGSMJSONStruct(t *testing.T) {
137342
ctx := context.Background()
138343
k8sClient := k8sfake.NewSimpleClientset()

0 commit comments

Comments
 (0)