sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/tags/tags.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package tags 18 19 import ( 20 "fmt" 21 "sort" 22 23 "github.com/aws/aws-sdk-go/aws" 24 "github.com/aws/aws-sdk-go/service/ec2" 25 "github.com/aws/aws-sdk-go/service/ec2/ec2iface" 26 "github.com/aws/aws-sdk-go/service/eks" 27 "github.com/aws/aws-sdk-go/service/eks/eksiface" 28 "github.com/pkg/errors" 29 30 infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1" 31 ) 32 33 var ( 34 // ErrBuildParamsRequired defines an error for when no build params are supplied. 35 ErrBuildParamsRequired = errors.New("no build params supplied") 36 37 // ErrApplyFuncRequired defines an error for when tags are not supplied. 38 ErrApplyFuncRequired = errors.New("no tags apply function supplied") 39 ) 40 41 // BuilderOption represents an option when creating a tags builder. 42 type BuilderOption func(*Builder) 43 44 // Builder is the interface for a tags builder. 45 type Builder struct { 46 params *infrav1.BuildParams 47 applyFunc func(params *infrav1.BuildParams) error 48 } 49 50 // New creates a new TagsBuilder with the specified build parameters 51 // and with optional configuration. 52 func New(params *infrav1.BuildParams, opts ...BuilderOption) *Builder { 53 builder := &Builder{ 54 params: params, 55 } 56 57 for _, opt := range opts { 58 opt(builder) 59 } 60 61 return builder 62 } 63 64 // Apply tags a resource with tags including the cluster tag. 65 func (b *Builder) Apply() error { 66 if b.params == nil { 67 return ErrBuildParamsRequired 68 } 69 if b.applyFunc == nil { 70 return ErrApplyFuncRequired 71 } 72 73 if err := b.applyFunc(b.params); err != nil { 74 return fmt.Errorf("failed applying tags: %w", err) 75 } 76 return nil 77 } 78 79 // Ensure applies the tags if the current tags differ from the params. 80 func (b *Builder) Ensure(current infrav1.Tags) error { 81 if b.params == nil { 82 return ErrBuildParamsRequired 83 } 84 if diff := computeDiff(current, *b.params); len(diff) > 0 { 85 return b.Apply() 86 } 87 return nil 88 } 89 90 // WithEC2 is used to denote that the tags builder will be using EC2. 91 func WithEC2(ec2client ec2iface.EC2API) BuilderOption { 92 return func(b *Builder) { 93 b.applyFunc = func(params *infrav1.BuildParams) error { 94 tags := infrav1.Build(*params) 95 awsTags := make([]*ec2.Tag, 0, len(tags)) 96 97 // For testing, we need sorted keys 98 sortedKeys := make([]string, 0, len(tags)) 99 for k := range tags { 100 sortedKeys = append(sortedKeys, k) 101 } 102 sort.Strings(sortedKeys) 103 104 for _, key := range sortedKeys { 105 tag := &ec2.Tag{ 106 Key: aws.String(key), 107 Value: aws.String(tags[key]), 108 } 109 awsTags = append(awsTags, tag) 110 } 111 112 createTagsInput := &ec2.CreateTagsInput{ 113 Resources: aws.StringSlice([]string{params.ResourceID}), 114 Tags: awsTags, 115 } 116 117 _, err := ec2client.CreateTags(createTagsInput) 118 return errors.Wrapf(err, "failed to tag resource %q in cluster %q", params.ResourceID, params.ClusterName) 119 } 120 } 121 } 122 123 // WithEKS is used to specify that the tags builder will be targeting EKS. 124 func WithEKS(eksclient eksiface.EKSAPI) BuilderOption { 125 return func(b *Builder) { 126 b.applyFunc = func(params *infrav1.BuildParams) error { 127 tags := infrav1.Build(*params) 128 129 eksTags := make(map[string]*string, len(tags)) 130 for k, v := range tags { 131 eksTags[k] = aws.String(v) 132 } 133 134 tagResourcesInput := &eks.TagResourceInput{ 135 ResourceArn: aws.String(params.ResourceID), 136 Tags: eksTags, 137 } 138 139 _, err := eksclient.TagResource(tagResourcesInput) 140 if err != nil { 141 return errors.Wrapf(err, "failed to tag eks cluster %q in cluster %q", params.ResourceID, params.ClusterName) 142 } 143 144 return nil 145 } 146 } 147 } 148 149 func computeDiff(current infrav1.Tags, buildParams infrav1.BuildParams) infrav1.Tags { 150 want := infrav1.Build(buildParams) 151 152 // Some tags could be external set by some external entities 153 // and that means even if there is no change in cluster 154 // managed tags, tags would be updated as "current" and 155 // "want" would be different due to external tags. 156 // This fix makes sure that tags are updated only if 157 // there is a change in cluster managed tags. 158 return want.Difference(current) 159 } 160 161 // BuildParamsToTagSpecification builds a TagSpecification for the specified resource type. 162 func BuildParamsToTagSpecification(ec2ResourceType string, params infrav1.BuildParams) *ec2.TagSpecification { 163 tags := infrav1.Build(params) 164 165 tagSpec := &ec2.TagSpecification{ResourceType: aws.String(ec2ResourceType)} 166 167 // For testing, we need sorted keys 168 sortedKeys := make([]string, 0, len(tags)) 169 for k := range tags { 170 sortedKeys = append(sortedKeys, k) 171 } 172 173 sort.Strings(sortedKeys) 174 175 for _, key := range sortedKeys { 176 tagSpec.Tags = append(tagSpec.Tags, &ec2.Tag{ 177 Key: aws.String(key), 178 Value: aws.String(tags[key]), 179 }) 180 } 181 182 return tagSpec 183 }