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  }