From 5494ae3aa0f6db287900a968100eb25fb43bcba2 Mon Sep 17 00:00:00 2001 From: Karuppiah Natarajan Date: Thu, 23 Sep 2021 19:05:58 +0530 Subject: [PATCH] fix resource group tags updation Signed-off-by: Karuppiah Natarajan --- api/v1alpha4/tags.go | 6 +++ azure/defaults.go | 5 +++ azure/scope/cluster.go | 36 ++++++++++++++++++ azure/scope/managedcontrolplane.go | 36 ++++++++++++++++++ azure/services/groups/groups.go | 59 +++++++++++++++++++++++++++++- azure/services/groups/spec.go | 1 + azure/services/tags/tags.go | 4 +- 7 files changed, 143 insertions(+), 4 deletions(-) diff --git a/api/v1alpha4/tags.go b/api/v1alpha4/tags.go index a7adad92047..32b5769eaf6 100644 --- a/api/v1alpha4/tags.go +++ b/api/v1alpha4/tags.go @@ -136,6 +136,12 @@ const ( // See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ // for annotation formatting rules. VMTagsLastAppliedAnnotation = "sigs.k8s.io/cluster-api-provider-azure-last-applied-tags-vm" + + // RGTagsLastAppliedAnnotation is the key for the Azure Cluster object annotation + // which tracks the AdditionalTags for Resource Group which is part in the Azure Cluster. + // See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + // for annotation formatting rules. + RGTagsLastAppliedAnnotation = "sigs.k8s.io/cluster-api-provider-azure-last-applied-tags-rg" ) // SpecVersionHashTagKey is the key for the spec version hash used to enable quick spec difference comparison. diff --git a/azure/defaults.go b/azure/defaults.go index 8643212cd7e..1c595a32cdc 100644 --- a/azure/defaults.go +++ b/azure/defaults.go @@ -172,6 +172,11 @@ func WithIndex(name string, n int) string { return fmt.Sprintf("%s-%d", name, n) } +// ResourceGroupID returns the azure resource ID for a given resource group. +func ResourceGroupID(subscriptionID, resourceGroup string) string { + return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", subscriptionID, resourceGroup) +} + // VMID returns the azure resource ID for a given VM. func VMID(subscriptionID, resourceGroup, vmName string) string { return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s", subscriptionID, resourceGroup, vmName) diff --git a/azure/scope/cluster.go b/azure/scope/cluster.go index 1685f26e275..a9544ce06b4 100644 --- a/azure/scope/cluster.go +++ b/azure/scope/cluster.go @@ -18,6 +18,7 @@ package scope import ( "context" + "encoding/json" "fmt" "hash/fnv" "strconv" @@ -747,3 +748,38 @@ func (s *ClusterScope) UpdatePatchStatus(condition clusterv1.ConditionType, serv conditions.MarkFalse(s.AzureCluster, condition, infrav1.FailedReason, clusterv1.ConditionSeverityError, "%s failed to update. err: %s", service, err.Error()) } } + +// AnnotationJSON returns a map[string]interface from a JSON annotation. +func (s *ClusterScope) AnnotationJSON(annotation string) (map[string]interface{}, error) { + out := map[string]interface{}{} + jsonAnnotation := s.AzureCluster.GetAnnotations()[annotation] + if len(jsonAnnotation) == 0 { + return out, nil + } + err := json.Unmarshal([]byte(jsonAnnotation), &out) + if err != nil { + return out, err + } + return out, nil +} + +// UpdateAnnotationJSON updates the `annotation` with +// `content`. `content` in this case should be a `map[string]interface{}` +// suitable for turning into JSON. This `content` map will be marshalled into a +// JSON string before being set as the given `annotation`. +func (s *ClusterScope) UpdateAnnotationJSON(annotation string, content map[string]interface{}) error { + b, err := json.Marshal(content) + if err != nil { + return err + } + s.SetAnnotation(annotation, string(b)) + return nil +} + +// SetAnnotation sets a key value annotation on the AzureCluster. +func (s *ClusterScope) SetAnnotation(key, value string) { + if s.AzureCluster.Annotations == nil { + s.AzureCluster.Annotations = map[string]string{} + } + s.AzureCluster.Annotations[key] = value +} diff --git a/azure/scope/managedcontrolplane.go b/azure/scope/managedcontrolplane.go index e0286f7f0ed..84cc913b23b 100644 --- a/azure/scope/managedcontrolplane.go +++ b/azure/scope/managedcontrolplane.go @@ -19,6 +19,7 @@ package scope import ( "context" "encoding/base64" + "encoding/json" "fmt" "net" "strings" @@ -603,3 +604,38 @@ func (s *ManagedControlPlaneScope) UpdatePutStatus(condition clusterv1.Condition func (s *ManagedControlPlaneScope) UpdatePatchStatus(condition clusterv1.ConditionType, service string, err error) { // TODO: add condition to AzureManagedControlPlane status } + +// AnnotationJSON returns a map[string]interface from a JSON annotation. +func (s *ManagedControlPlaneScope) AnnotationJSON(annotation string) (map[string]interface{}, error) { + out := map[string]interface{}{} + jsonAnnotation := s.ControlPlane.GetAnnotations()[annotation] + if len(jsonAnnotation) == 0 { + return out, nil + } + err := json.Unmarshal([]byte(jsonAnnotation), &out) + if err != nil { + return out, err + } + return out, nil +} + +// UpdateAnnotationJSON updates the `annotation` with +// `content`. `content` in this case should be a `map[string]interface{}` +// suitable for turning into JSON. This `content` map will be marshalled into a +// JSON string before being set as the given `annotation`. +func (s *ManagedControlPlaneScope) UpdateAnnotationJSON(annotation string, content map[string]interface{}) error { + b, err := json.Marshal(content) + if err != nil { + return err + } + s.SetAnnotation(annotation, string(b)) + return nil +} + +// SetAnnotation sets a key value annotation on the ControlPlane. +func (s *ManagedControlPlaneScope) SetAnnotation(key, value string) { + if s.ControlPlane.Annotations == nil { + s.ControlPlane.Annotations = map[string]string{} + } + s.ControlPlane.Annotations[key] = value +} diff --git a/azure/services/groups/groups.go b/azure/services/groups/groups.go index aaba4805761..14c8a56c809 100644 --- a/azure/services/groups/groups.go +++ b/azure/services/groups/groups.go @@ -19,6 +19,7 @@ package groups import ( "context" + "github.com/Azure/go-autorest/autorest/to" "github.com/go-logr/logr" "github.com/pkg/errors" @@ -26,6 +27,7 @@ import ( "sigs.k8s.io/cluster-api-provider-azure/azure" "sigs.k8s.io/cluster-api-provider-azure/azure/converters" "sigs.k8s.io/cluster-api-provider-azure/azure/services/async" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/tags" "sigs.k8s.io/cluster-api-provider-azure/util/reconciler" "sigs.k8s.io/cluster-api-provider-azure/util/tele" ) @@ -36,6 +38,7 @@ const serviceName = "group" type Service struct { Scope GroupScope client + tagsSvc azure.Reconciler } // GroupScope defines the scope interface for a group service. @@ -44,14 +47,41 @@ type GroupScope interface { azure.Authorizer azure.AsyncStatusUpdater GroupSpec() azure.ResourceSpecGetter + AdditionalTags() infrav1.Tags ClusterName() string + AnnotationJSON(string) (map[string]interface{}, error) + UpdateAnnotationJSON(string, map[string]interface{}) error +} + +// GroupTagScope defines the scope for a group service with additional information about tags. +type GroupTagScope struct { + GroupScope +} + +func (s *GroupTagScope) TagsSpecs() []azure.TagsSpec { + return []azure.TagsSpec{ + { + Scope: azure.ResourceGroupID(s.SubscriptionID(), s.GroupSpec().ResourceName()), + Tags: infrav1.Build(infrav1.BuildParams{ + ClusterName: s.ClusterName(), + Lifecycle: infrav1.ResourceLifecycleOwned, + Name: to.StringPtr(s.GroupSpec().ResourceName()), + Role: to.StringPtr(infrav1.CommonRole), + Additional: s.AdditionalTags(), + }), + Annotation: infrav1.RGTagsLastAppliedAnnotation, + }, + } } // New creates a new service. func New(scope GroupScope) *Service { + groupTagScope := &GroupTagScope{scope} + return &Service{ - Scope: scope, - client: newClient(scope), + Scope: scope, + client: newClient(scope), + tagsSvc: tags.New(groupTagScope), } } @@ -67,9 +97,34 @@ func (s *Service) Reconcile(ctx context.Context) error { err := async.CreateResource(ctx, s.Scope, s.client, groupSpec, serviceName) s.Scope.UpdatePutStatus(infrav1.ResourceGroupReadyCondition, serviceName, err) + + if err != nil { + return err + } + + err = s.ReconcileTags(ctx) + return err } +func (s *Service) ReconcileTags(ctx context.Context) error { + // check that the resource group is not BYO. + managed, err := s.IsGroupManaged(ctx) + if err != nil { + return errors.Wrap(err, "could not get resource group management state") + } + if !managed { + s.Scope.V(2).Info("Should not update resource group tags in unmanaged mode") + return azure.ErrNotOwned + } + + if err := s.tagsSvc.Reconcile(ctx); err != nil { + return errors.Wrap(err, "unable to update tags") + } + + return nil +} + // Delete deletes the resource group if it is managed by capz. func (s *Service) Delete(ctx context.Context) error { ctx, span := tele.Tracer().Start(ctx, "groups.Service.Delete") diff --git a/azure/services/groups/spec.go b/azure/services/groups/spec.go index b61a3d97639..fa6be878198 100644 --- a/azure/services/groups/spec.go +++ b/azure/services/groups/spec.go @@ -51,6 +51,7 @@ func (s *GroupSpec) OwnerResourceName() string { func (s *GroupSpec) Parameters(existing interface{}) (interface{}, error) { if existing != nil { // rg already exists, nothing to update. + // Note that rg tags are updated separately using tags service return nil, nil } return resources.Group{ diff --git a/azure/services/tags/tags.go b/azure/services/tags/tags.go index c3e8bacadd5..92684a89e6c 100644 --- a/azure/services/tags/tags.go +++ b/azure/services/tags/tags.go @@ -31,7 +31,7 @@ import ( // TagScope defines the scope interface for a tags service. type TagScope interface { logr.Logger - azure.ClusterDescriber + azure.Authorizer TagsSpecs() []azure.TagsSpec AnnotationJSON(string) (map[string]interface{}, error) UpdateAnnotationJSON(string, map[string]interface{}) error @@ -94,7 +94,7 @@ func (s *Service) Reconcile(ctx context.Context) error { return nil } -// Delete is a no-op as the tags get deleted as part of VM deletion. +// Delete is a no-op as the tags get deleted as part of resource deletion. func (s *Service) Delete(ctx context.Context) error { _, span := tele.Tracer().Start(ctx, "tags.Service.Delete") defer span.End()