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 }