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  }