From 43ad4ae5c941f8a87ae0efa3b6c83d0c6f299f26 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Sun, 19 Feb 2023 12:11:12 +0100 Subject: [PATCH] Add support for tagging EBS volumes (#199) --- .gitignore | 1 + README.MD | 18 ++++++++ cmd/ebs.go | 69 +++++++++++++++++++++++++++++++ pkg/ebs.go | 82 ++++++++++++++++++++++++++++++++++++ pkg/ebs_test.go | 108 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 278 insertions(+) create mode 100644 cmd/ebs.go create mode 100644 pkg/ebs.go create mode 100644 pkg/ebs_test.go diff --git a/.gitignore b/.gitignore index 0c4c985..21d5d79 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.dll *.so *.dylib +awstaghelper # Test binary, built with `go test -c` *.test diff --git a/README.MD b/README.MD index ea3f692..60a6767 100644 --- a/README.MD +++ b/README.MD @@ -28,6 +28,8 @@ Tags are critical to managing AWS resources at scale. Awstaghelper provides a co * [Iam Role](#iam-role) * [Elastic Beanstalk](#elastic-beanstalk) * [ECR](#ecr) + * [AutoScaling groups](#autoscaling-groups) + * [EBS Volumes](#ebs-volumes) * [Global parameters](#global-parameters) * [Contributing](#contributing) * [License](#license) @@ -322,6 +324,22 @@ Read csv and tag ASGs - `awstaghelper asg tag-asg` Example: `awstaghelper asg tag-asg --filename asgTags.csv --profile main` +### EBS Volumes + +#### Get EBS volume tags + +Get list of EBS volumes with required tags - `awstaghelper ebs get-ebs-tags` + +Example: +`awstaghelper ebs get-ebs-tags --filename ebsTags.csv --tags Name,Owner --profile main` + +#### Tag EBS volumes + +Read csv and tag EBS volumes - `awstaghelper ebs tag-ebs` + +Example: +`awstaghelper ebs tag-ebs --filename ebsTags.csv --profile main` + ## Global parameters `filename` - path where to write or read data. Supported by every option. Default `awsTags.csv` diff --git a/cmd/ebs.go b/cmd/ebs.go new file mode 100644 index 0000000..9f77e0d --- /dev/null +++ b/cmd/ebs.go @@ -0,0 +1,69 @@ +/* +Copyright © 2023 Cristian Magherusan-Stanciu cristi@leanercloud.com +Copyright © 2020 Maksym Postument 777rip777@gmail.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package cmd is the package for the CLI of awstaghelper +package cmd + +import ( + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/mpostument/awstaghelper/pkg" + + "github.com/spf13/cobra" +) + +// ebsCmd represents the ebs command +var ebsCmd = &cobra.Command{ + Use: "ebs", + Short: "Root command for interaction with AWS EBS volumes", + Long: `Root command for interaction with AWS EBS volumes,`, +} + +var getEBSVolumeCmd = &cobra.Command{ + Use: "get-ebs-tags", + Short: "Write EBS volume IDs and required tags to CSV", + Long: `Write to csv data with EBS volume IDs and required tags to CSV. This CSV can be used with tag-ebs command to tag AWS environment. Specify list of tags which should be read using tags flag: --tags Name,Env,Project. Csv filename can be specified with flag filename.`, + Run: func(cmd *cobra.Command, args []string) { + tags, _ := cmd.Flags().GetString("tags") + filename, _ := cmd.Flags().GetString("filename") + profile, _ := cmd.Flags().GetString("profile") + region, _ := cmd.Flags().GetString("region") + sess := pkg.GetSession(region, profile) + client := ec2.New(sess) + pkg.WriteCsv(pkg.ParseEBSVolumeTags(tags, client), filename) + }, +} + +var tagEBSVolumeCmd = &cobra.Command{ + Use: "tag-ebs", + Short: "Read CSV and tag EBS volumes with CSV data", + Long: `Read CSV generated with get-ebs-tags command and tag EBS volumes with tags from CSV.`, + Run: func(cmd *cobra.Command, args []string) { + filename, _ := cmd.Flags().GetString("filename") + profile, _ := cmd.Flags().GetString("profile") + region, _ := cmd.Flags().GetString("region") + sess := pkg.GetSession(region, profile) + csvData := pkg.ReadCsv(filename) + client := ec2.New(sess) + pkg.TagEBSVolumes(csvData, client) + }, +} + +func init() { + rootCmd.AddCommand(ebsCmd) + ebsCmd.AddCommand(getEBSVolumeCmd) + ebsCmd.AddCommand(tagEBSVolumeCmd) +} diff --git a/pkg/ebs.go b/pkg/ebs.go new file mode 100644 index 0000000..427b36a --- /dev/null +++ b/pkg/ebs.go @@ -0,0 +1,82 @@ +/* +Copyright © 2023 Cristian Magherusan-Stanciu cristi@leanercloud.com +Copyright © 2020 Maksym Postument 777rip777@gmail.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pkg + +import ( + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" +) + +// getEBSVolumes returns all EBS volumes from specified region +func getEBSVolumes(client ec2iface.EC2API) []*ec2.Volume { + input := &ec2.DescribeVolumesInput{} + + var result []*ec2.Volume + + err := client.DescribeVolumesPages(input, + func(page *ec2.DescribeVolumesOutput, lastPage bool) bool { + result = append(result, page.Volumes...) + return !lastPage + }) + if err != nil { + log.Fatal("Not able to get EBS volumes ", err) + } + return result +} + +// ParseEBSVolumeTags parse output from getEBSVolumes and return volume ID and specified tags. +func ParseEBSVolumeTags(tagsToRead string, client ec2iface.EC2API) [][]string { + volumesOutput := getEBSVolumes(client) + rows := addHeadersToCsv(tagsToRead, "VolumeId") + for _, volume := range volumesOutput { + tags := map[string]string{} + for _, tag := range volume.Tags { + tags[*tag.Key] = *tag.Value + } + rows = addTagsToCsv(tagsToRead, tags, rows, *volume.VolumeId) + } + return rows +} + +// TagEBSVolumes tag EBS volumes. Take as input data from csv file. Where first column is volume ID. +func TagEBSVolumes(csvData [][]string, client ec2iface.EC2API) { + for r := 1; r < len(csvData); r++ { + var tags []*ec2.Tag + for c := 1; c < len(csvData[0]); c++ { + tags = append(tags, &ec2.Tag{ + Key: &csvData[0][c], + Value: &csvData[r][c], + }) + } + + input := &ec2.CreateTagsInput{ + Resources: []*string{ + aws.String(csvData[r][0]), + }, + Tags: tags, + } + + _, err := client.CreateTags(input) + if awsErrorHandle(err) { + return + } + } +} diff --git a/pkg/ebs_test.go b/pkg/ebs_test.go new file mode 100644 index 0000000..d79faef --- /dev/null +++ b/pkg/ebs_test.go @@ -0,0 +1,108 @@ +/* +Copyright © 2023 Cristian Magherusan-Stanciu cristi@leanercloud.com +Copyright © 2020 Maksym Postument 777rip777@gmail.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pkg + +import ( + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/stretchr/testify/assert" +) + +type mockedEBS struct { + ec2iface.EC2API + respDescribeVolumes ec2.DescribeVolumesOutput +} + +// AWS Mocks +func (m *mockedEBS) DescribeVolumesPages(input *ec2.DescribeVolumesInput, + pageFunc func(*ec2.DescribeVolumesOutput, bool) bool) error { + pageFunc(&m.respDescribeVolumes, true) + return nil +} + +var ParseEBSVolumeTagsResponse = ec2.DescribeVolumesOutput{ + Volumes: []*ec2.Volume{ + { + VolumeId: aws.String("vol-1"), + Tags: []*ec2.Tag{ + { + Key: aws.String("Name"), + Value: aws.String("Volume1"), + }, + { + Key: aws.String("Environment"), + Value: aws.String("Test"), + }, + }, + }, + { + VolumeId: aws.String("vol-2"), + Tags: []*ec2.Tag{ + { + Key: aws.String("Name"), + Value: aws.String("Volume2"), + }, + { + Key: aws.String("Environment"), + Value: aws.String("Dev"), + }, + }, + }, + }, +} + +func Test_getEBSVolumes(t *testing.T) { + cases := []*mockedEBS{ + { + respDescribeVolumes: ParseEBSVolumeTagsResponse, + }, + } + + expectedResult := ParseEBSVolumeTagsResponse.Volumes + for _, c := range cases { + t.Run("getEBSVolumes", func(t *testing.T) { + result := getEBSVolumes(c) + assertions := assert.New(t) + assertions.EqualValues(expectedResult, result) + }) + + } +} + +func TestParseEBSVolumeTags(t *testing.T) { + cases := []*mockedEBS{ + { + respDescribeVolumes: ParseEBSVolumeTagsResponse, + }, + } + expectedResult := [][]string{ + {"VolumeId", "Name", "Environment"}, + {"vol-1", "Volume1", "Test"}, + {"vol-2", "Volume2", "Dev"}, + } + for _, c := range cases { + t.Run("ParseEBSVolumeTags", func(t *testing.T) { + result := ParseEBSVolumeTags("Name,Environment", c) + assertions := assert.New(t) + assertions.EqualValues(expectedResult, result) + }) + } +}