Skip to content

Commit 9ec122c

Browse files
committed
Allow csv tags
1 parent 45db664 commit 9ec122c

File tree

6 files changed

+164
-10
lines changed

6 files changed

+164
-10
lines changed

README.md

+2-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ The `k8s-aws-ebs-tagger` watches for new PersistentVolumeClaims and when new AWS
1010

1111
#### cmdline args
1212

13-
`--default-tags` - A json encoded key/value map of the tags to set by default on EBS Volumes. Values can be overwritten by the `aws-ebs-tagger/tags` annotation.
13+
`--default-tags` - A json or csv encoded key/value map of the tags to set by default on EBS Volumes. Values can be overwritten by the `aws-ebs-tagger/tags` annotation.
14+
`--tag-format` - Either `json` or `csv` for the format the `aws-ebs-tagger/tags` and `--default-tags` are in.
1415

1516
#### Annotations
1617

@@ -52,9 +53,6 @@ helm install k8s-aws-ebs-tagger mtougeron/k8s-aws-ebs-tagger
5253

5354
Images are available on the [GitHub Container Registry](https://github.com/users/mtougeron/packages/container/k8s-aws-ebs-tagger/versions) and [DockerHub](https://hub.docker.com/repository/docker/mtougeron/k8s-aws-ebs-tagger). Containers are published for `linux/amd64` & `linux/arm64`.
5455

55-
### Known Issues
56-
57-
It is currently only possible ([#9](https://github.com/mtougeron/k8s-aws-ebs-tagger/issues/9)) to watch all namespaces or a single namespace. This will be addressed in a future version of the app.
5856

5957
### Licensing
6058

go.sum

+1
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,7 @@ k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
792792
k8s.io/apimachinery v0.0.0-20190817020851-f2f3a405f61d/go.mod h1:3jediapYqJ2w1BFw7lAZPCx7scubsTfosqHkhXCWJKw=
793793
k8s.io/apimachinery v0.20.1 h1:LAhz8pKbgR8tUwn7boK+b2HZdt7MiTu2mkYtFMUjTRQ=
794794
k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
795+
k8s.io/apimachinery v0.20.2 h1:hFx6Sbt1oG0n6DZ+g4bFt5f6BoMkOjKWsQFu077M3Vg=
795796
k8s.io/client-go v0.0.0-20190819141724-e14f31a72a77 h1:w1BoabVnPpPqQCY3sHK4qVwa12Lk8ip1pKMR1C+qbdo=
796797
k8s.io/client-go v0.0.0-20190819141724-e14f31a72a77/go.mod h1:DmkJD5UDP87MVqUQ5VJ6Tj9Oen8WzXPhk3la4qpyG4g=
797798
k8s.io/client-go v0.20.1 h1:Qquik0xNFbK9aUG92pxHYsyfea5/RPO9o9bSywNor+M=

kubernetes.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,13 @@ func buildTags(pvc *corev1.PersistentVolumeClaim) map[string]string {
189189
log.Debugln("Does not have " + annotationPrefix + "/tags annotation")
190190
return tags
191191
}
192-
err := json.Unmarshal([]byte(tagString), &customTags)
193-
if err != nil {
194-
log.Errorln("Failed to Unmarshal JSON:", err)
192+
if tagFormat == "csv" {
193+
customTags = parseCsv(tagString)
194+
} else {
195+
err := json.Unmarshal([]byte(tagString), &customTags)
196+
if err != nil {
197+
log.Errorln("Failed to Unmarshal JSON:", err)
198+
}
195199
}
196200

197201
for k, v := range customTags {
@@ -235,6 +239,7 @@ func provisionedByAwsEbs(pvc *corev1.PersistentVolumeClaim) bool {
235239

236240
func processPersistentVolumeClaim(pvc *corev1.PersistentVolumeClaim) (string, map[string]string, error) {
237241
tags := buildTags(pvc)
242+
238243
log.WithFields(log.Fields{"namespace": pvc.GetNamespace(), "pvc": pvc.GetName(), "tags": tags}).Debugln("PVC Tags")
239244

240245
pv, err := k8sClient.CoreV1().PersistentVolumes().Get(context.TODO(), pvc.Spec.VolumeName, metav1.GetOptions{})

kubernetes_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ func Test_buildTags(t *testing.T) {
150150
defaultTags map[string]string
151151
annotations map[string]string
152152
want map[string]string
153+
tagFormat string
153154
}{
154155
{
155156
name: "ignore annotation set",
@@ -229,14 +230,57 @@ func Test_buildTags(t *testing.T) {
229230
annotations: map[string]string{"aws-ebs-tagger/tags": "{\"something\": \"else\"}"},
230231
want: map[string]string{"something": "else"},
231232
},
233+
{
234+
name: "tags annotation not set with default tags - csv",
235+
defaultTags: map[string]string{"foo": "bar", "something": "else"},
236+
annotations: map[string]string{},
237+
want: map[string]string{"foo": "bar", "something": "else"},
238+
tagFormat: "csv",
239+
},
240+
{
241+
name: "tags annotation not set with no default tags - csv",
242+
defaultTags: map[string]string{},
243+
annotations: map[string]string{},
244+
want: map[string]string{},
245+
tagFormat: "csv",
246+
},
247+
{
248+
name: "tags annotation set with default tags - csv",
249+
defaultTags: map[string]string{"foo": "bar"},
250+
annotations: map[string]string{"aws-ebs-tagger/tags": "something=else"},
251+
want: map[string]string{"foo": "bar", "something": "else"},
252+
tagFormat: "csv",
253+
},
254+
{
255+
name: "tags annotation set with default tags with override - csv",
256+
defaultTags: map[string]string{"foo": "foo"},
257+
annotations: map[string]string{"aws-ebs-tagger/tags": "foo=bar,something=else"},
258+
want: map[string]string{"foo": "bar", "something": "else"},
259+
tagFormat: "csv",
260+
},
261+
{
262+
name: "tags annotation set with invalid tags - csv",
263+
defaultTags: map[string]string{},
264+
annotations: map[string]string{"aws-ebs-tagger/tags": "{\"foo\": \"bar\"}"},
265+
want: map[string]string{},
266+
tagFormat: "csv",
267+
},
268+
269+
// foo=bar,something=else
232270
}
233271
for _, tt := range tests {
234272
t.Run(tt.name, func(t *testing.T) {
235273
pvc.SetAnnotations(tt.annotations)
236274
defaultTags = tt.defaultTags
275+
if tt.tagFormat != "" {
276+
tagFormat = tt.tagFormat
277+
} else {
278+
tagFormat = "json"
279+
}
237280
if got := buildTags(pvc); !reflect.DeepEqual(got, tt.want) {
238281
t.Errorf("buildTags() = %v, want %v", got, tt.want)
239282
}
283+
tagFormat = "json"
240284
})
241285
}
242286
}

main.go

+34-3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ var (
5151
defaultTags map[string]string
5252
annotationPrefix string = "aws-ebs-tagger"
5353
watchNamespace string
54+
tagFormat string = "json"
5455

5556
promActionsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
5657
Name: "k8s_aws_ebs_tagger_actions_total",
@@ -108,6 +109,7 @@ func main() {
108109
flag.StringVar(&leaseLockName, "lease-lock-name", "k8s-aws-ebs-tagger", "the lease lock resource name")
109110
flag.StringVar(&leaseLockNamespace, "lease-lock-namespace", os.Getenv("NAMESPACE"), "the lease lock resource namespace")
110111
flag.StringVar(&defaultTagsString, "default-tags", "", "Default tags to add to EBS volume")
112+
flag.StringVar(&tagFormat, "tag-format", "json", "Whether the tags are in json or csv format. Default: json")
111113
flag.StringVar(&annotationPrefix, "annotation-prefix", "aws-ebs-tagger", "Annotation prefix to check")
112114
flag.StringVar(&watchNamespace, "watch-namespace", os.Getenv("WATCH_NAMESPACE"), "A specific namespace to watch (default is all namespaces)")
113115
flag.StringVar(&statusPort, "status-port", "8000", "The healthz port")
@@ -124,11 +126,16 @@ func main() {
124126
}
125127
}
126128

129+
defaultTags = make(map[string]string)
127130
if defaultTagsString != "" {
128131
log.Debugln("defaultTagsString:", defaultTagsString)
129-
err := json.Unmarshal([]byte(defaultTagsString), &defaultTags)
130-
if err != nil {
131-
log.Fatalln("default-tags are not valid json key/value pairs:", err)
132+
if tagFormat == "csv" {
133+
defaultTags = parseCsv(defaultTagsString)
134+
} else {
135+
err := json.Unmarshal([]byte(defaultTagsString), &defaultTags)
136+
if err != nil {
137+
log.Fatalln("default-tags are not valid json key/value pairs:", err)
138+
}
132139
}
133140
}
134141
log.WithFields(log.Fields{"tags": defaultTags}).Infoln("Default Tags")
@@ -278,3 +285,27 @@ func runWatchNamespaceTask(ctx context.Context, namespace string) {
278285
<-ctx.Done()
279286
close(ch)
280287
}
288+
289+
func parseCsv(value string) map[string]string {
290+
291+
tags := make(map[string]string)
292+
for _, s := range strings.Split(value, ",") {
293+
if len(s) == 0 {
294+
continue
295+
}
296+
pairs := strings.SplitN(s, "=", 2)
297+
if len(pairs) != 2 {
298+
log.Errorln("invalid csv key/value pair. Skipping...")
299+
continue
300+
}
301+
k := strings.TrimSpace(pairs[0])
302+
v := strings.TrimSpace(pairs[1])
303+
if k == "" || v == "" {
304+
log.Errorln("invalid csv key/value pair. Skipping...")
305+
continue
306+
}
307+
tags[k] = v
308+
}
309+
310+
return tags
311+
}

main_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Licensed to Michael Tougeron <[email protected]> under
2+
// one or more contributor license agreements. See the LICENSE
3+
// file distributed with this work for additional information
4+
// regarding copyright ownership.
5+
// Michael Tougeron <[email protected]> licenses this file
6+
// to you under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing,
13+
// software distributed under the License is distributed on an
14+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
// KIND, either express or implied. See the License for the
16+
// specific language governing permissions and limitations
17+
// under the License.
18+
19+
package main
20+
21+
import (
22+
"reflect"
23+
"testing"
24+
)
25+
26+
func Test_parseCsv(t *testing.T) {
27+
tests := []struct {
28+
name string
29+
csv string
30+
want map[string]string
31+
}{
32+
{
33+
name: "empty string",
34+
csv: "",
35+
want: map[string]string{},
36+
},
37+
{
38+
name: "single key/value string",
39+
csv: "touge=me",
40+
want: map[string]string{"touge": "me"},
41+
},
42+
{
43+
name: "multiple key/value string",
44+
csv: "touge=me,foo=bar",
45+
want: map[string]string{"touge": "me", "foo": "bar"},
46+
},
47+
{
48+
name: "invalid string",
49+
csv: "foo",
50+
want: map[string]string{},
51+
},
52+
{
53+
name: "invalid key string",
54+
csv: "=foo",
55+
want: map[string]string{},
56+
},
57+
{
58+
name: "invalid value string",
59+
csv: "foo=",
60+
want: map[string]string{},
61+
},
62+
{
63+
name: "double delim",
64+
csv: "foo=bar,,touge=me",
65+
want: map[string]string{"touge": "me", "foo": "bar"},
66+
},
67+
}
68+
for _, tt := range tests {
69+
t.Run(tt.name, func(t *testing.T) {
70+
if got := parseCsv(tt.csv); !reflect.DeepEqual(got, tt.want) {
71+
t.Errorf("parseCsv() = %v, want %v", got, tt.want)
72+
}
73+
})
74+
}
75+
}

0 commit comments

Comments
 (0)