sigs.k8s.io/cluster-api-provider-aws@v1.5.5/exp/controllers/awsmachinepool_tags.go (about)

     1  /*
     2  Copyright 2020 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 controllers
    18  
    19  import (
    20  	"encoding/json"
    21  
    22  	expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/exp/api/v1beta1"
    23  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services"
    24  )
    25  
    26  const (
    27  	// TagsLastAppliedAnnotation is the key for the AWSMachinePool object annotation
    28  	// which tracks the tags that the AWSMachinePool actuator is responsible
    29  	// for. These are the tags that have been handled by the
    30  	// AdditionalTags in the AWSMachinePool Provider Config.
    31  	// See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
    32  	// for annotation formatting rules.
    33  	TagsLastAppliedAnnotation = "sigs.k8s.io/cluster-api-provider-aws-last-applied-tags"
    34  )
    35  
    36  // Ensure that the tags of the AWSMachinePool are correct
    37  // Returns bool, error
    38  // Bool indicates if changes were made or not, allowing the caller to decide
    39  // if the machine should be updated.
    40  func (r *AWSMachinePoolReconciler) ensureTags(ec2svc services.EC2Interface, asgsvc services.ASGInterface, machinePool *expinfrav1.AWSMachinePool, launchTemplateID, asgName *string, additionalTags map[string]string) (bool, error) {
    41  	annotation, err := r.machinePoolAnnotationJSON(machinePool, TagsLastAppliedAnnotation)
    42  	if err != nil {
    43  		return false, err
    44  	}
    45  
    46  	// Check if the instance tags were changed. If they were, update them.
    47  	// It would be possible here to only send new/updated tags, but for the
    48  	// moment we send everything, even if only a single tag was created or
    49  	// upated.
    50  	changed, created, deleted, newAnnotation := tagsChanged(annotation, additionalTags)
    51  	if changed {
    52  		err = ec2svc.UpdateResourceTags(launchTemplateID, created, deleted)
    53  		if err != nil {
    54  			return false, err
    55  		}
    56  
    57  		if err := asgsvc.UpdateResourceTags(asgName, created, deleted); err != nil {
    58  			return false, err
    59  		}
    60  
    61  		// We also need to update the annotation if anything changed.
    62  		err = r.updateMachinePoolAnnotationJSON(machinePool, TagsLastAppliedAnnotation, newAnnotation)
    63  		if err != nil {
    64  			return false, err
    65  		}
    66  	}
    67  
    68  	return changed, nil
    69  }
    70  
    71  // tagsChanged determines which tags to delete and which to add.
    72  func tagsChanged(annotation map[string]interface{}, src map[string]string) (bool, map[string]string, map[string]string, map[string]interface{}) {
    73  	// Bool tracking if we found any changed state.
    74  	changed := false
    75  
    76  	// Tracking for created/updated
    77  	created := map[string]string{}
    78  
    79  	// Tracking for tags that were deleted.
    80  	deleted := map[string]string{}
    81  
    82  	// The new annotation that we need to set if anything is created/updated.
    83  	newAnnotation := map[string]interface{}{}
    84  
    85  	// Loop over annotation, checking if entries are in src.
    86  	// If an entry is present in annotation but not src, it has been deleted
    87  	// since last time. We flag this in the deleted map.
    88  	for t, v := range annotation {
    89  		_, ok := src[t]
    90  
    91  		// Entry isn't in src, it has been deleted.
    92  		if !ok {
    93  			// Cast v to a string here. This should be fine, tags are always
    94  			// strings.
    95  			deleted[t] = v.(string)
    96  			changed = true
    97  		}
    98  	}
    99  
   100  	// Loop over src, checking for entries in annotation.
   101  	//
   102  	// If an entry is in src, but not annotation, it has been created since
   103  	// last time.
   104  	//
   105  	// If an entry is in both src and annotation, we compare their values, if
   106  	// the value in src differs from that in annotation, the tag has been
   107  	// updated since last time.
   108  	for t, v := range src {
   109  		av, ok := annotation[t]
   110  
   111  		// Entries in the src always need to be noted in the newAnnotation. We
   112  		// know they're going to be created or updated.
   113  		newAnnotation[t] = v
   114  
   115  		// Entry isn't in annotation, it's new.
   116  		if !ok {
   117  			created[t] = v
   118  			newAnnotation[t] = v
   119  			changed = true
   120  			continue
   121  		}
   122  
   123  		// Entry is in annotation, has the value changed?
   124  		if v != av {
   125  			created[t] = v
   126  			changed = true
   127  		}
   128  
   129  		// Entry existed in both src and annotation, and their values were
   130  		// equal. Nothing to do.
   131  	}
   132  
   133  	// We made it through the loop, and everything that was in src, was also
   134  	// in dst. Nothing changed.
   135  	return changed, created, deleted, newAnnotation
   136  }
   137  
   138  // updateMachinePoolAnnotationJSON updates the `annotation` on `machinePool` with
   139  // `content`. `content` in this case should be a `map[string]interface{}`
   140  // suitable for turning into JSON. This `content` map will be marshalled into a
   141  // JSON string before being set as the given `annotation`.
   142  func (r *AWSMachinePoolReconciler) updateMachinePoolAnnotationJSON(machinePool *expinfrav1.AWSMachinePool, annotation string, content map[string]interface{}) error {
   143  	b, err := json.Marshal(content)
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	r.updateMachinePoolAnnotation(machinePool, annotation, string(b))
   149  	return nil
   150  }
   151  
   152  // updateMachinePoolAnnotation updates the `annotation` on the given `machinePool` with
   153  // `content`.
   154  func (r *AWSMachinePoolReconciler) updateMachinePoolAnnotation(machinePool *expinfrav1.AWSMachinePool, annotation, content string) {
   155  	// Get the annotations
   156  	annotations := machinePool.GetAnnotations()
   157  
   158  	if annotations == nil {
   159  		annotations = make(map[string]string)
   160  	}
   161  
   162  	// Set our annotation to the given content.
   163  	annotations[annotation] = content
   164  
   165  	// Update the machine object with these annotations
   166  	machinePool.SetAnnotations(annotations)
   167  }
   168  
   169  // Returns a map[string]interface from a JSON annotation.
   170  // This method gets the given `annotation` from the `machinePool` and unmarshalls it
   171  // from a JSON string into a `map[string]interface{}`.
   172  func (r *AWSMachinePoolReconciler) machinePoolAnnotationJSON(machinePool *expinfrav1.AWSMachinePool, annotation string) (map[string]interface{}, error) {
   173  	out := map[string]interface{}{}
   174  
   175  	jsonAnnotation := r.machinePoolAnnotation(machinePool, annotation)
   176  	if len(jsonAnnotation) == 0 {
   177  		return out, nil
   178  	}
   179  
   180  	err := json.Unmarshal([]byte(jsonAnnotation), &out)
   181  	if err != nil {
   182  		return out, err
   183  	}
   184  
   185  	return out, nil
   186  }
   187  
   188  // Fetches the specific machine annotation.
   189  func (r *AWSMachinePoolReconciler) machinePoolAnnotation(machinePool *expinfrav1.AWSMachinePool, annotation string) string {
   190  	return machinePool.GetAnnotations()[annotation]
   191  }