Skip to content

Commit

Permalink
Adding Feature To Modify Tags Of Existing Volumes Through VolumeAttri…
Browse files Browse the repository at this point in the history
…butesClass
  • Loading branch information
mdzraf committed Jul 12, 2024
1 parent 2ab9ab7 commit 957d509
Show file tree
Hide file tree
Showing 25 changed files with 469 additions and 119 deletions.
8 changes: 4 additions & 4 deletions charts/aws-ebs-csi-driver/templates/controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ spec:
{{- with .Values.controller.volumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.controller.dnsConfig }}
dnsConfig:
{{- toYaml .Values.controller.dnsConfig | nindent 4 }}
{{- end }}
{{- if .Values.controller.dnsConfig }}
dnsConfig:
{{- toYaml .Values.controller.dnsConfig | nindent 8 }}
{{- end }}
4 changes: 2 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func main() {

switch cmd {
case "pre-stop-hook":
clientset, clientErr := metadata.DefaultKubernetesAPIClient()
clientset, clientErr := metadata.DefaultKubernetesAPIClient(options.Kubeconfig)()
if clientErr != nil {
klog.ErrorS(err, "unable to communicate with k8s API")
} else {
Expand Down Expand Up @@ -140,7 +140,7 @@ func main() {

cfg := metadata.MetadataServiceConfig{
EC2MetadataClient: metadata.DefaultEC2MetadataClient,
K8sAPIClient: metadata.DefaultKubernetesAPIClient,
K8sAPIClient: metadata.DefaultKubernetesAPIClient(options.Kubeconfig),
}

region := os.Getenv("AWS_REGION")
Expand Down
10 changes: 1 addition & 9 deletions docs/example-iam-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,7 @@
"Resource": [
"arn:aws:ec2:*:*:volume/*",
"arn:aws:ec2:*:*:snapshot/*"
],
"Condition": {
"StringEquals": {
"ec2:CreateAction": [
"CreateVolume",
"CreateSnapshot"
]
}
}
]
},
{
"Effect": "Allow",
Expand Down
31 changes: 31 additions & 0 deletions docs/tagging.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,37 @@ backup=true
billingID=ABCDEF
```

# Adding, Modifying, and Deleting Tags Of Existing Volumes
The AWS EBS CSI Driver supports the modifying of tags of existing volumes through `VolumeAttributesClass.parameters` the examples below show the syntax for addition, modification, and deletion of tags within the `VolumeAttributesClass.parameters`. For a walkthrough on how to apply these modifications to a volume follow the [walkthrough for Volume Modification via VolumeAttributeClass](../examples/kubernetes/modify-volume)

**Syntax for Adding or Modifying a Tag**

If a key has the prefix `tagSpecification`, the CSI driver will treat the value as a key-value pair to be added to the existing volume. If there is already an existing tag with the specified key, the CSI driver will overwrite the value of that tag with the new value specified.
```
apiVersion: storage.k8s.io/v1alpha1
kind: VolumeAttributesClass
metadata:
name: io2-class
driverName: ebs.csi.aws.com
parameters:
tagSpecification_1: "location=Seattle"
tagSpecification_2: "cost-center=" // If the value is left blank, tag is created with an empty value
```
**Syntax for Deleting a Tag**

If a key has the prefix `tagDeletion`, the CSI driver will treat the value as a tag key, and the existing tag with that key will be removed from the volume.
```
apiVersion: storage.k8s.io/v1alpha1
kind: VolumeAttributesClass
metadata:
name: io2-class
driverName: ebs.csi.aws.com
parameters:
tagDeletion_1: "location" // Deletes tag with key "location"
tagDeletion_2: "cost-center"
```


# Snapshot Tagging
The AWS EBS CSI Driver supports tagging snapshots through `VolumeSnapshotClass.parameters`, similarly to StorageClass tagging.

Expand Down
10 changes: 7 additions & 3 deletions examples/kubernetes/modify-volume/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,25 @@ This example will only work on a cluster with the `VolumeAttributesClass` featur
Mon Feb 26 22:28:39 UTC 2024
...
```
4. Deploy the `VolumeAttributesClass`
```sh
$ kubectl apply -f manifests/volumeattributesclass.yaml
```

4. Simultaneously, deploy the `VolumeAttributesClass` and edit the `PersistentVolumeClaim` to point to this class
5. Edit the `PersistentVolumeClaim` to point to this class
```sh
$ kubectl patch pvc ebs-claim --patch '{"spec": {"volumeAttributesClassName": "io2-class"}}'
persistentvolumeclaim/ebs-claim patched
```

5. Wait for the `VolumeAttributesClass` to apply to the volume
6. Wait for the `VolumeAttributesClass` to apply to the volume
```sh
$ kubectl get pvc ebs-claim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
ebs-claim Bound pvc-076b2d14-b643-47d4-a2ce-fbf9cd36572b 100Gi RWO ebs-sc io2-class 5m54s
```

6. (Optional) Delete example resources
7. (Optional) Delete example resources
```sh
$ kubectl delete -f manifests
storageclass.storage.k8s.io "ebs-sc" deleted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ driverName: ebs.csi.aws.com
parameters:
type: io2
iops: "10000"
tagSpecification_1: "location=Seattle"
tagSpecification_2: "cost-center="
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0
go.opentelemetry.io/otel/sdk v1.27.0
golang.org/x/sys v0.21.0
google.golang.org/grpc v1.64.0
google.golang.org/grpc v1.64.1
google.golang.org/protobuf v1.34.2
k8s.io/api v0.31.0-alpha.2
k8s.io/apimachinery v0.31.0-alpha.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1913,8 +1913,8 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
Expand Down
10 changes: 1 addition & 9 deletions hack/e2e/kops/patch-cluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,7 @@ spec:
"Resource": [
"arn:aws:ec2:*:*:volume/*",
"arn:aws:ec2:*:*:snapshot/*"
],
"Condition": {
"StringEquals": {
"ec2:CreateAction": [
"CreateVolume",
"CreateSnapshot"
]
}
}
]
},
{
"Effect": "Allow",
Expand Down
42 changes: 42 additions & 0 deletions pkg/cloud/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ type ModifyDiskOptions struct {
Throughput int32
}

// ModifyTagsOptions represents parameter to modify the tags of an existing EBS volume
type ModifyTagsOptions struct {
TagsToAdd map[string]string
TagsToDelete []string
}

// Snapshot represents an EBS volume snapshot
type Snapshot struct {
SnapshotID string
Expand Down Expand Up @@ -746,6 +752,42 @@ func (c *cloud) batchDescribeVolumesModifications(request *ec2.DescribeVolumesMo
return r.Result, nil
}

// ModifyTags adds, updates, and deletes tags for the specified EBS volume.
func (c *cloud) ModifyTags(ctx context.Context, volumeID string, tagOptions ModifyTagsOptions) error {
if len(tagOptions.TagsToDelete) > 0 {
deleteTagsInput := &ec2.DeleteTagsInput{
Resources: []string{volumeID},
Tags: make([]types.Tag, 0, len(tagOptions.TagsToDelete)),
}
for _, tagKey := range tagOptions.TagsToDelete {
deleteTagsInput.Tags = append(deleteTagsInput.Tags, types.Tag{Key: aws.String(tagKey)})
}
_, deleteErr := c.ec2.DeleteTags(ctx, deleteTagsInput)
if deleteErr != nil {
klog.ErrorS(deleteErr, "failed to delete tags", "volumeID", volumeID)
return deleteErr
}
}
if len(tagOptions.TagsToAdd) > 0 {
createTagsInput := &ec2.CreateTagsInput{
Resources: []string{volumeID},
Tags: make([]types.Tag, 0, len(tagOptions.TagsToAdd)),
}
for k, v := range tagOptions.TagsToAdd {
createTagsInput.Tags = append(createTagsInput.Tags, types.Tag{
Key: aws.String(k),
Value: aws.String(v),
})
}
_, addErr := c.ec2.CreateTags(ctx, createTagsInput)
if addErr != nil {
klog.ErrorS(addErr, "failed to create tags", "volumeID", volumeID)
return addErr
}
}
return nil
}

// ResizeOrModifyDisk resizes an EBS volume in GiB increments, rounding up to the next possible allocatable unit, and/or modifies an EBS
// volume with the parameters in ModifyDiskOptions.
// The resizing operation is performed only when newSizeBytes != 0.
Expand Down
115 changes: 115 additions & 0 deletions pkg/cloud/cloud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2509,6 +2509,121 @@ func TestResizeOrModifyDisk(t *testing.T) {
}
}

func TestModifyTags(t *testing.T) {
validTagsToAddInput := map[string]string{
"key1": "value1",
"key2": "value2",
"key3": "",
}

validTagsToDeleteInput := []string{
"key1",
"key2",
}

emptyTagsToAddInput := map[string]string{}
emptyTagsToDeleteInput := []string{}

testCases := []struct {
name string
volumeID string
negativeCase bool
modifyDiskOptions *ModifyDiskOptions
modifyTagsOptions ModifyTagsOptions
expErr error
mockEC2 *MockEC2API
volumes []types.Volume
}{
{
name: "success normal tag addition",
volumeID: "mod-tag-test-name",
modifyTagsOptions: ModifyTagsOptions{
TagsToAdd: validTagsToAddInput,
},
expErr: nil,
},
{
name: "success normal tag deletion",
volumeID: "mod-tag-test-name",
modifyTagsOptions: ModifyTagsOptions{
TagsToDelete: validTagsToDeleteInput,
},
expErr: nil,
},
{
name: "success normal tag addition and tag deletion",
volumeID: "mod-tag-test-name",
modifyTagsOptions: ModifyTagsOptions{
TagsToAdd: validTagsToAddInput,
TagsToDelete: validTagsToDeleteInput,
},
expErr: nil,
},
{
name: "fail: EC2 API generic error TagsToAdd",
volumeID: "mod-tag-test-name",
negativeCase: true,
expErr: fmt.Errorf("Generic EC2 API error"),
modifyTagsOptions: ModifyTagsOptions{
TagsToAdd: validTagsToAddInput,
TagsToDelete: emptyTagsToDeleteInput,
},
},
{
name: "fail: EC2 API generic error TagsToDelete",
volumeID: "mod-tag-test-name",
negativeCase: true,
expErr: fmt.Errorf("Generic EC2 API error"),
modifyTagsOptions: ModifyTagsOptions{
TagsToAdd: emptyTagsToAddInput,
TagsToDelete: validTagsToDeleteInput,
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
mockEC2 := NewMockEC2API(mockCtrl)
c := newCloud(mockEC2)

ctx := context.Background()

if len(tc.modifyTagsOptions.TagsToAdd) > 0 {
if tc.negativeCase {
mockEC2.EXPECT().CreateTags(gomock.Any(), gomock.Any()).Return(nil, tc.expErr).Times(1)
} else {
mockEC2.EXPECT().CreateTags(gomock.Any(), gomock.Any()).Return(&ec2.CreateTagsOutput{}, tc.expErr).Times(1)
}
}
if len(tc.modifyTagsOptions.TagsToDelete) > 0 {
if tc.negativeCase {
mockEC2.EXPECT().DeleteTags(gomock.Any(), gomock.Any()).Return(nil, tc.expErr).Times(1)
} else {
mockEC2.EXPECT().DeleteTags(gomock.Any(), gomock.Any()).Return(&ec2.DeleteTagsOutput{}, tc.expErr).Times(1)
}
}

err := c.ModifyTags(ctx, tc.volumeID, tc.modifyTagsOptions)
if err != nil {
if tc.expErr == nil {
t.Fatalf("ModifyTags() failed: expected no error, got: %v", err)
} else {
if !strings.Contains(err.Error(), tc.expErr.Error()) {
t.Fatalf("ModifyTags() failed: expected error %v, got: %v", tc.expErr, err)
}
}
} else {
if tc.expErr != nil {
t.Fatal("ModifyTags() failed: expected error, got nothing")
}
}

mockCtrl.Finish()
})
}
}

func TestGetSnapshotByName(t *testing.T) {
testCases := []struct {
name string
Expand Down
1 change: 1 addition & 0 deletions pkg/cloud/ec2_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ type EC2API interface {
DescribeVolumesModifications(ctx context.Context, params *ec2.DescribeVolumesModificationsInput, optFns ...func(*ec2.Options)) (*ec2.DescribeVolumesModificationsOutput, error)
DescribeTags(ctx context.Context, params *ec2.DescribeTagsInput, optFns ...func(*ec2.Options)) (*ec2.DescribeTagsOutput, error)
CreateTags(ctx context.Context, params *ec2.CreateTagsInput, optFns ...func(*ec2.Options)) (*ec2.CreateTagsOutput, error)
DeleteTags(ctx context.Context, params *ec2.DeleteTagsInput, optFns ...func(*ec2.Options)) (*ec2.DeleteTagsOutput, error)
EnableFastSnapshotRestores(ctx context.Context, params *ec2.EnableFastSnapshotRestoresInput, optFns ...func(*ec2.Options)) (*ec2.EnableFastSnapshotRestoresOutput, error)
}
1 change: 1 addition & 0 deletions pkg/cloud/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Cloud interface {
DeleteDisk(ctx context.Context, volumeID string) (success bool, err error)
AttachDisk(ctx context.Context, volumeID string, nodeID string) (devicePath string, err error)
DetachDisk(ctx context.Context, volumeID string, nodeID string) (err error)
ModifyTags(ctx context.Context, volumeID string, tagOptions ModifyTagsOptions) (err error)
ResizeOrModifyDisk(ctx context.Context, volumeID string, newSizeBytes int64, options *ModifyDiskOptions) (newSize int32, err error)
WaitForAttachmentState(ctx context.Context, volumeID, expectedState string, expectedInstance string, expectedDevice string, alreadyAssigned bool) (*types.VolumeAttachment, error)
GetDiskByName(ctx context.Context, name string, capacityBytes int64) (disk *Disk, err error)
Expand Down
Loading

0 comments on commit 957d509

Please sign in to comment.