Skip to content

Commit 766f8e4

Browse files
wadhwakabirKabir Wadhwa
and
Kabir Wadhwa
authored
Add support for AWS EFS tagging (mtougeron#55)
* Feature/efs (mtougeron#1) * update for efs support Co-authored-by: Kabir Wadhwa <[email protected]> * Main fixes (mtougeron#2) Add EFS support * fixes * changes for efs volume id * changes for return logic * fix test cases Co-authored-by: Kabir Wadhwa <[email protected]>
1 parent a32ae7e commit 766f8e4

File tree

6 files changed

+302
-31
lines changed

6 files changed

+302
-31
lines changed

README.md

+10-10
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,42 @@ A utility to tag PVC volumes based on the PVC's `k8s-pvc-tagger/tags` annotation
66

77
![Go](https://github.com/mtougeron/k8s-pvc-tagger/workflows/Go/badge.svg) ![Gosec](https://github.com/mtougeron/k8s-pvc-tagger/workflows/Gosec/badge.svg) ![ContainerScan](https://github.com/mtougeron/k8s-pvc-tagger/workflows/ContainerScan/badge.svg) [![GitHub tag](https://img.shields.io/github/v/tag/mtougeron/k8s-pvc-tagger)](https://github.com/mtougeron/k8s-pvc-tagger/tags/)
88

9-
The `k8s-pvc-tagger` watches for new PersistentVolumeClaims and when new AWS EBS volumes are created it adds tags based on the PVC's `k8s-pvc-tagger/tags` annotation to the created EBS volume. Other cloud provider and volume times are coming soon.
9+
The `k8s-pvc-tagger` watches for new PersistentVolumeClaims and when new AWS EBS/EFS volumes are created it adds tags based on the PVC's `k8s-pvc-tagger/tags` annotation to the created EBS/EFS volume. Other cloud provider and volume times are coming soon.
1010

1111
### How to set tags
1212

1313
#### cmdline args
1414

15-
`--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 `k8s-pvc-tagger/tags` annotation.
15+
`--default-tags` - A json or csv encoded key/value map of the tags to set by default on EBS/EFS Volumes. Values can be overwritten by the `k8s-pvc-tagger/tags` annotation.
1616

1717
`--tag-format` - Either `json` or `csv` for the format the `k8s-pvc-tagger/tags` and `--default-tags` are in.
1818

19-
`--allow-all-tags` - Allow all tags to be set via the PVC; even those used by the EBS controllers. Use with caution!
19+
`--allow-all-tags` - Allow all tags to be set via the PVC; even those used by the EBS/EFS controllers. Use with caution!
2020

2121
#### Annotations
2222

2323
`k8s-pvc-tagger/ignore` - When this annotation is set (any value) it will ignore this PVC and not add any tags to it
2424

25-
`k8s-pvc-tagger/tags` - A json encoded key/value map of the tags to set on the EBS Volume (in addition to the `--default-tags`). It can also be used to override the values set in the `--default-tags`
25+
`k8s-pvc-tagger/tags` - A json encoded key/value map of the tags to set on the EBS/EFS Volume (in addition to the `--default-tags`). It can also be used to override the values set in the `--default-tags`
2626

2727
NOTE: Until version `v1.2.0` the legacy annotation prefix of `aws-ebs-tagger` will continue to be supported for aws-ebs volumes ONLY.
2828

2929
#### Examples
3030

3131
1. The cmdline arg `--default-tags={"me": "touge"}` and no annotation will set the tag `me=touge`
3232

33-
2. The cmdline arg `--default-tags={"me": "touge"}` and the annotation `k8s-pvc-tagger/tags: | {"me": "someone else", "another tag": "some value"}` will create the tags `me=someone else` and `another tag=some value` on the EBS Volume
33+
2. The cmdline arg `--default-tags={"me": "touge"}` and the annotation `k8s-pvc-tagger/tags: | {"me": "someone else", "another tag": "some value"}` will create the tags `me=someone else` and `another tag=some value` on the EBS/EFS Volume
3434

35-
3. The cmdline arg `--default-tags={"me": "touge"}` and the annotation `k8s-pvc-tagger/ignore: ""` will not set any tags on the EBS Volume
35+
3. The cmdline arg `--default-tags={"me": "touge"}` and the annotation `k8s-pvc-tagger/ignore: ""` will not set any tags on the EBS/EFS Volume
3636

37-
4. The cmdline arg `--default-tags={"me": "touge"}` and the annotation `k8s-pvc-tagger/tags: | {"cost-center": "abc", "environment": "prod"}` will create the tags `me=touge`, `cost-center=abc` and `environment=prod` on the EBS Volume
37+
4. The cmdline arg `--default-tags={"me": "touge"}` and the annotation `k8s-pvc-tagger/tags: | {"cost-center": "abc", "environment": "prod"}` will create the tags `me=touge`, `cost-center=abc` and `environment=prod` on the EBS/EFS Volume
3838

3939
#### ignored tags
4040

4141
The following tags are ignored by default
42-
- `kubernetes.io/*`
43-
- `KubernetesCluster`
44-
- `Name`
42+
- `kubernetes.io/*`
43+
- `KubernetesCluster`
44+
- `Name`
4545

4646
#### Tag Templates
4747

aws.go

+63-7
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,29 @@ import (
2828
"github.com/aws/aws-sdk-go/aws/session"
2929
"github.com/aws/aws-sdk-go/service/ec2"
3030
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
31+
"github.com/aws/aws-sdk-go/service/efs"
32+
"github.com/aws/aws-sdk-go/service/efs/efsiface"
3133
"github.com/prometheus/client_golang/prometheus"
3234
log "github.com/sirupsen/logrus"
3335
)
3436

3537
var (
3638
// awsSession the AWS Session
3739
awsSession *session.Session
38-
ec2Client *Client
3940
)
4041

4142
const (
4243
// Matching strings for region
4344
regexpAWSRegion = `^[\w]{2}[-][\w]{4,9}[-][\d]$`
4445
)
4546

47+
// Client efs interface
48+
type EFSClient struct {
49+
efsiface.EFSAPI
50+
}
51+
4652
// Client EC2 client interface
47-
type Client struct {
53+
type EBSClient struct {
4854
ec2iface.EC2API
4955
}
5056

@@ -71,10 +77,16 @@ func createAWSSession(awsRegion string) *session.Session {
7177
return session.Must(session.NewSession(awsConfig))
7278
}
7379

80+
// newEFSClient initializes an EFS client
81+
func newEFSClient() (*EFSClient, error) {
82+
svc := efs.New(awsSession)
83+
return &EFSClient{svc}, nil
84+
}
85+
7486
// newEC2Client initializes an EC2 client
75-
func newEC2Client() (*Client, error) {
87+
func newEC2Client() (*EBSClient, error) {
7688
svc := ec2.New(awsSession)
77-
return &Client{svc}, nil
89+
return &EBSClient{svc}, nil
7890
}
7991

8092
func getMetadataRegion() (string, error) {
@@ -90,7 +102,7 @@ func getMetadataRegion() (string, error) {
90102
return doc.Region, nil
91103
}
92104

93-
func (client *Client) addVolumeTags(volumeID string, tags map[string]string, storageclass string) {
105+
func (client *EBSClient) addEBSVolumeTags(volumeID string, tags map[string]string, storageclass string) {
94106
var ec2Tags []*ec2.Tag
95107
for k, v := range tags {
96108
ec2Tags = append(ec2Tags, &ec2.Tag{Key: aws.String(k), Value: aws.String(v)})
@@ -112,7 +124,7 @@ func (client *Client) addVolumeTags(volumeID string, tags map[string]string, sto
112124
promActionsLegacyTotal.With(prometheus.Labels{"status": "success"}).Inc()
113125
}
114126

115-
func (client *Client) deleteVolumeTags(volumeID string, tags []string, storageclass string) {
127+
func (client *EBSClient) deleteEBSVolumeTags(volumeID string, tags []string, storageclass string) {
116128
var ec2Tags []*ec2.Tag
117129
for _, k := range tags {
118130
ec2Tags = append(ec2Tags, &ec2.Tag{Key: aws.String(k)})
@@ -124,7 +136,51 @@ func (client *Client) deleteVolumeTags(volumeID string, tags []string, storagecl
124136
Tags: ec2Tags,
125137
})
126138
if err != nil {
127-
log.Errorln("Could not delete tags for volumeID:", volumeID, err)
139+
log.Errorln("Could not EBS delete tags for volumeID:", volumeID, err)
140+
promActionsTotal.With(prometheus.Labels{"status": "error", "storageclass": storageclass}).Inc()
141+
promActionsLegacyTotal.With(prometheus.Labels{"status": "error"}).Inc()
142+
return
143+
}
144+
145+
promActionsTotal.With(prometheus.Labels{"status": "success", "storageclass": storageclass}).Inc()
146+
promActionsLegacyTotal.With(prometheus.Labels{"status": "success"}).Inc()
147+
}
148+
149+
func (client *EFSClient) addEFSVolumeTags(volumeID string, tags map[string]string, storageclass string) {
150+
var efsTags []*efs.Tag
151+
for k, v := range tags {
152+
efsTags = append(efsTags, &efs.Tag{Key: aws.String(k), Value: aws.String(v)})
153+
}
154+
155+
// Add tags to the volume
156+
_, err := client.TagResource(&efs.TagResourceInput{
157+
ResourceId: aws.String(volumeID),
158+
Tags: efsTags,
159+
})
160+
if err != nil {
161+
log.Errorln("Could not EFS create tags for volumeID:", volumeID, err)
162+
promActionsTotal.With(prometheus.Labels{"status": "error", "storageclass": storageclass}).Inc()
163+
promActionsLegacyTotal.With(prometheus.Labels{"status": "error"}).Inc()
164+
return
165+
}
166+
167+
promActionsTotal.With(prometheus.Labels{"status": "success", "storageclass": storageclass}).Inc()
168+
promActionsLegacyTotal.With(prometheus.Labels{"status": "success"}).Inc()
169+
}
170+
171+
func (client *EFSClient) deleteEFSVolumeTags(volumeID string, tags []string, storageclass string) {
172+
var efsTags []*string
173+
for _, k := range tags {
174+
efsTags = append(efsTags, aws.String(k))
175+
}
176+
177+
// Add tags to the volume
178+
_, err := client.UntagResource(&efs.UntagResourceInput{
179+
ResourceId: aws.String(volumeID),
180+
TagKeys: efsTags,
181+
})
182+
if err != nil {
183+
log.Errorln("Could not EFS delete tags for volumeID:", volumeID, err)
128184
promActionsTotal.With(prometheus.Labels{"status": "error", "storageclass": storageclass}).Inc()
129185
promActionsLegacyTotal.With(prometheus.Labels{"status": "error"}).Inc()
130186
return

examples/iam-role.json

+12-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@
1111
"Resource": [
1212
"arn:aws:ec2:*:*:volume/*"
1313
]
14+
},
15+
{
16+
"Sid": "",
17+
"Effect": "Allow",
18+
"Action": [
19+
"elasticfilesystem:TagResource",
20+
"elasticfilesystem:UntagResource",
21+
],
22+
"Resource": [
23+
"arn:aws:elasticfilesystem:*:*:access-point/*"
24+
]
1425
}
1526
]
16-
}
27+
}

kubernetes.go

+48-8
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ var (
5151
const (
5252
// Matching strings for volume operations.
5353
regexpAWSVolumeID = `^aws:\/\/\w{2}-\w{4,9}-\d\w\/(vol-\w+)$`
54+
regexpEFSVolumeID = `^fs-\w+::(fsap-\w+)$`
5455
)
5556

5657
type TagTemplate struct {
@@ -99,12 +100,13 @@ func watchForPersistentVolumeClaims(ch chan struct{}, watchNamespace string) {
99100

100101
informer := factory.Core().V1().PersistentVolumeClaims().Informer()
101102

103+
efsClient, _ := newEFSClient()
102104
ec2Client, _ := newEC2Client()
103105

104106
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
105107
AddFunc: func(obj interface{}) {
106108
pvc := obj.(*corev1.PersistentVolumeClaim)
107-
if !provisionedByAwsEbs(pvc) {
109+
if !provisionedByAwsEfs(pvc) && !provisionedByAwsEbs(pvc) {
108110
return
109111
}
110112
log.WithFields(log.Fields{"namespace": pvc.GetNamespace(), "pvc": pvc.GetName()}).Infoln("New PVC Added to Store")
@@ -113,7 +115,12 @@ func watchForPersistentVolumeClaims(ch chan struct{}, watchNamespace string) {
113115
if err != nil || len(tags) == 0 {
114116
return
115117
}
116-
ec2Client.addVolumeTags(volumeID, tags, *pvc.Spec.StorageClassName)
118+
if provisionedByAwsEfs(pvc) {
119+
efsClient.addEFSVolumeTags(volumeID, tags, *pvc.Spec.StorageClassName)
120+
}
121+
if provisionedByAwsEbs(pvc) {
122+
ec2Client.addEBSVolumeTags(volumeID, tags, *pvc.Spec.StorageClassName)
123+
}
117124
},
118125
UpdateFunc: func(old, new interface{}) {
119126

@@ -123,7 +130,7 @@ func watchForPersistentVolumeClaims(ch chan struct{}, watchNamespace string) {
123130
log.WithFields(log.Fields{"namespace": newPVC.GetNamespace(), "pvc": newPVC.GetName()}).Debugln("ResourceVersion are the same")
124131
return
125132
}
126-
if !provisionedByAwsEbs(newPVC) {
133+
if !provisionedByAwsEfs(newPVC) && !provisionedByAwsEbs(newPVC) {
127134
return
128135
}
129136
if newPVC.Spec.VolumeName == "" {
@@ -141,9 +148,13 @@ func watchForPersistentVolumeClaims(ch chan struct{}, watchNamespace string) {
141148
return
142149
}
143150
if len(tags) > 0 {
144-
ec2Client.addVolumeTags(volumeID, tags, *newPVC.Spec.StorageClassName)
151+
if provisionedByAwsEfs(newPVC) {
152+
efsClient.addEFSVolumeTags(volumeID, tags, *newPVC.Spec.StorageClassName)
153+
}
154+
if provisionedByAwsEbs(newPVC) {
155+
ec2Client.addEBSVolumeTags(volumeID, tags, *newPVC.Spec.StorageClassName)
156+
}
145157
}
146-
147158
oldTags := buildTags(oldPVC)
148159
var deletedTags []string
149160
for k := range oldTags {
@@ -152,15 +163,20 @@ func watchForPersistentVolumeClaims(ch chan struct{}, watchNamespace string) {
152163
}
153164
}
154165
if len(deletedTags) > 0 {
155-
ec2Client.deleteVolumeTags(volumeID, deletedTags, *oldPVC.Spec.StorageClassName)
166+
if provisionedByAwsEfs(newPVC) {
167+
efsClient.deleteEFSVolumeTags(volumeID, deletedTags, *oldPVC.Spec.StorageClassName)
168+
}
169+
if provisionedByAwsEbs(newPVC) {
170+
ec2Client.deleteEBSVolumeTags(volumeID, deletedTags, *oldPVC.Spec.StorageClassName)
171+
}
156172
}
157173
},
158174
})
159175

160176
informer.Run(ch)
161177
}
162178

163-
func parseAWSVolumeID(k8sVolumeID string) string {
179+
func parseAWSEBSVolumeID(k8sVolumeID string) string {
164180
re := regexp.MustCompile(regexpAWSVolumeID)
165181
matches := re.FindSubmatch([]byte(k8sVolumeID))
166182
if len(matches) <= 1 {
@@ -170,6 +186,16 @@ func parseAWSVolumeID(k8sVolumeID string) string {
170186
return string(matches[1])
171187
}
172188

189+
func parseAWSEFSVolumeID(k8sVolumeID string) string {
190+
re := regexp.MustCompile(regexpEFSVolumeID)
191+
matches := re.FindSubmatch([]byte(k8sVolumeID))
192+
if len(matches) <= 1 {
193+
log.Errorln("Can't parse valid AWS EFS volumeID:", k8sVolumeID)
194+
return ""
195+
}
196+
return string(matches[1])
197+
}
198+
173199
func buildTags(pvc *corev1.PersistentVolumeClaim) map[string]string {
174200

175201
tags := map[string]string{}
@@ -290,6 +316,18 @@ func isValidTagName(name string) bool {
290316
return true
291317
}
292318

319+
func provisionedByAwsEfs(pvc *corev1.PersistentVolumeClaim) bool {
320+
annotations := pvc.GetAnnotations()
321+
if provisionedBy, ok := annotations["volume.beta.kubernetes.io/storage-provisioner"]; !ok {
322+
log.WithFields(log.Fields{"namespace": pvc.GetNamespace(), "pvc": pvc.GetName()}).Debugln("no volume.beta.kubernetes.io/storage-provisioner annotation")
323+
return false
324+
} else if provisionedBy == "efs.csi.aws.com" {
325+
log.WithFields(log.Fields{"namespace": pvc.GetNamespace(), "pvc": pvc.GetName()}).Debugln("efs.csi.aws.com volume")
326+
return true
327+
}
328+
return false
329+
}
330+
293331
func provisionedByAwsEbs(pvc *corev1.PersistentVolumeClaim) bool {
294332
annotations := pvc.GetAnnotations()
295333
if provisionedBy, ok := annotations["volume.beta.kubernetes.io/storage-provisioner"]; !ok {
@@ -327,8 +365,10 @@ func processPersistentVolumeClaim(pvc *corev1.PersistentVolumeClaim) (string, ma
327365
return "", nil, errors.New("cannot get volume.beta.kubernetes.io/storage-provisioner annotation")
328366
} else if provisionedBy == "ebs.csi.aws.com" {
329367
volumeID = pv.Spec.PersistentVolumeSource.CSI.VolumeHandle
368+
} else if provisionedBy == "efs.csi.aws.com" {
369+
volumeID = parseAWSEFSVolumeID(pv.Spec.PersistentVolumeSource.CSI.VolumeHandle)
330370
} else if provisionedBy == "kubernetes.io/aws-ebs" {
331-
volumeID = parseAWSVolumeID(pv.Spec.PersistentVolumeSource.AWSElasticBlockStore.VolumeID)
371+
volumeID = parseAWSEBSVolumeID(pv.Spec.PersistentVolumeSource.AWSElasticBlockStore.VolumeID)
332372
}
333373
log.WithFields(log.Fields{"namespace": pvc.GetNamespace(), "pvc": pvc.GetName(), "volumeID": volumeID}).Debugln("parsed volumeID:", volumeID)
334374
if len(volumeID) == 0 {

0 commit comments

Comments
 (0)