sigs.k8s.io/cluster-api-provider-azure@v1.14.3/azure/services/tags/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 tags 18 19 import ( 20 "context" 21 22 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" 23 "github.com/pkg/errors" 24 "k8s.io/utils/ptr" 25 "sigs.k8s.io/cluster-api-provider-azure/azure" 26 "sigs.k8s.io/cluster-api-provider-azure/azure/converters" 27 "sigs.k8s.io/cluster-api-provider-azure/util/tele" 28 ) 29 30 const serviceName = "tags" 31 32 // TagScope defines the scope interface for a tags service. 33 type TagScope interface { 34 azure.Authorizer 35 ClusterName() string 36 TagsSpecs() []azure.TagsSpec 37 AnnotationJSON(string) (map[string]interface{}, error) 38 UpdateAnnotationJSON(string, map[string]interface{}) error 39 } 40 41 // Service provides operations on Azure resources. 42 type Service struct { 43 Scope TagScope 44 client 45 } 46 47 // New creates a new service. 48 func New(scope TagScope) (*Service, error) { 49 cli, err := NewClient(scope) 50 if err != nil { 51 return nil, err 52 } 53 return &Service{ 54 Scope: scope, 55 client: cli, 56 }, nil 57 } 58 59 // Name returns the service name. 60 func (s *Service) Name() string { 61 return serviceName 62 } 63 64 // Some resource types are always assumed to be managed by CAPZ whether or not 65 // they have the canonical "owned" tag applied to most resources. The annotation 66 // key for those types should be listed here so their tags are always 67 // interpreted as managed. 68 var alwaysManagedAnnotations = map[string]struct{}{ 69 azure.ManagedClusterTagsLastAppliedAnnotation: {}, 70 } 71 72 // Reconcile ensures tags are correct. 73 func (s *Service) Reconcile(ctx context.Context) error { 74 ctx, log, done := tele.StartSpanWithLogger(ctx, "tags.Service.Reconcile") 75 defer done() 76 77 for _, tagsSpec := range s.Scope.TagsSpecs() { 78 existingTags, err := s.client.GetAtScope(ctx, tagsSpec.Scope) 79 if err != nil { 80 return errors.Wrap(err, "failed to get existing tags") 81 } 82 tags := make(map[string]*string) 83 if existingTags.Properties != nil && existingTags.Properties.Tags != nil { 84 tags = existingTags.Properties.Tags 85 } 86 87 if _, alwaysManaged := alwaysManagedAnnotations[tagsSpec.Annotation]; !alwaysManaged && !s.isResourceManaged(tags) { 88 log.V(4).Info("Skipping tags reconcile for not managed resource") 89 continue 90 } 91 92 lastAppliedTags, err := s.Scope.AnnotationJSON(tagsSpec.Annotation) 93 if err != nil { 94 return err 95 } 96 changed, createdOrUpdated, deleted, newAnnotation := TagsChanged(lastAppliedTags, tagsSpec.Tags, tags) 97 if changed { 98 log.V(2).Info("Updating tags") 99 if len(createdOrUpdated) > 0 { 100 createdOrUpdatedTags := make(map[string]*string) 101 for k, v := range createdOrUpdated { 102 createdOrUpdatedTags[k] = ptr.To(v) 103 } 104 105 if _, err := s.client.UpdateAtScope(ctx, tagsSpec.Scope, armresources.TagsPatchResource{Operation: ptr.To(armresources.TagsPatchOperationMerge), Properties: &armresources.Tags{Tags: createdOrUpdatedTags}}); err != nil { 106 return errors.Wrap(err, "cannot update tags") 107 } 108 } 109 110 if len(deleted) > 0 { 111 deletedTags := make(map[string]*string) 112 for k, v := range deleted { 113 deletedTags[k] = ptr.To(v) 114 } 115 116 if _, err := s.client.UpdateAtScope(ctx, tagsSpec.Scope, armresources.TagsPatchResource{Operation: ptr.To(armresources.TagsPatchOperationDelete), Properties: &armresources.Tags{Tags: deletedTags}}); err != nil { 117 return errors.Wrap(err, "cannot update tags") 118 } 119 } 120 log.V(2).Info("successfully updated tags") 121 } 122 123 // We also need to update the annotation even if nothing changed to 124 // ensure it's set immediately following resource creation. 125 if err := s.Scope.UpdateAnnotationJSON(tagsSpec.Annotation, newAnnotation); err != nil { 126 return err 127 } 128 } 129 return nil 130 } 131 132 func (s *Service) isResourceManaged(tags map[string]*string) bool { 133 return converters.MapToTags(tags).HasOwned(s.Scope.ClusterName()) 134 } 135 136 // Delete is a no-op as the tags get deleted as part of VM deletion. 137 func (s *Service) Delete(ctx context.Context) error { 138 _, _, done := tele.StartSpanWithLogger(ctx, "tags.Service.Delete") 139 defer done() 140 141 return nil 142 } 143 144 // TagsChanged determines which tags to delete and which to add. 145 func TagsChanged(lastAppliedTags map[string]interface{}, desiredTags map[string]string, currentTags map[string]*string) (change bool, createOrUpdates map[string]string, deletes map[string]string, annotation map[string]interface{}) { 146 // Bool tracking if we found any changed state. 147 changed := false 148 149 // Tracking for created/updated 150 createdOrUpdated := map[string]string{} 151 152 // Tracking for tags that were deleted. 153 deleted := map[string]string{} 154 155 // The new annotation that we need to set if anything is created/updated. 156 newAnnotation := map[string]interface{}{} 157 158 // Loop over lastAppliedTags, checking if entries are in desiredTags. 159 // If an entry is present in lastAppliedTags but not in desiredTags, it has been deleted 160 // since last time. We flag this in the deleted map. 161 for t, v := range lastAppliedTags { 162 _, ok := desiredTags[t] 163 164 // Entry isn't in desiredTags, it has been deleted. 165 if !ok { 166 // Cast v to a string here. This should be fine, tags are always 167 // strings. 168 deleted[t] = v.(string) 169 changed = true 170 } 171 } 172 173 // Loop over desiredTags, checking for entries in currentTags. 174 // 175 // If an entry is in desiredTags, but not currentTags, it has been created since 176 // last time, or some external entity deleted it. 177 // 178 // If an entry is in both desiredTags and currentTags, we compare their values, if 179 // the value in desiredTags differs from that in currentTags, the tag has been 180 // updated since last time or some external entity modified it. 181 for t, v := range desiredTags { 182 av, ok := currentTags[t] 183 184 // Entries in the desiredTags always need to be noted in the newAnnotation. We 185 // know they're going to be created or updated. 186 newAnnotation[t] = v 187 188 // Entry isn't in desiredTags, it's new. 189 if !ok { 190 createdOrUpdated[t] = v 191 newAnnotation[t] = v 192 changed = true 193 continue 194 } 195 196 // Entry is in desiredTags, has the value changed? 197 if v != *av { 198 createdOrUpdated[t] = v 199 changed = true 200 } 201 202 // Entry existed in both desiredTags and desiredTags, and their values were 203 // equal. Nothing to do. 204 } 205 206 // We made it through the loop, and everything that was in desiredTags, was also 207 // in dst. Nothing changed. 208 return changed, createdOrUpdated, deleted, newAnnotation 209 } 210 211 // IsManaged returns always returns true as CAPZ does not support BYO tags. 212 func (s *Service) IsManaged(ctx context.Context) (bool, error) { 213 return true, nil 214 }