Skip to content

Commit

Permalink
fix resource group tags updation
Browse files Browse the repository at this point in the history
Signed-off-by: Karuppiah Natarajan <[email protected]>
  • Loading branch information
karuppiah7890 committed Sep 24, 2021
1 parent 0ea1c6a commit f8b14fa
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 3 deletions.
6 changes: 6 additions & 0 deletions api/v1alpha4/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions azure/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
52 changes: 52 additions & 0 deletions azure/scope/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package scope

import (
"context"
"encoding/json"
"fmt"
"hash/fnv"
"strconv"
Expand Down Expand Up @@ -747,3 +748,54 @@ 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
}

func (s *ClusterScope) TagsSpecs() []azure.TagsSpec {
return []azure.TagsSpec{
{
Scope: azure.ResourceGroupID(s.SubscriptionID(), s.ResourceGroup()),
Tags: infrav1.Build(infrav1.BuildParams{
ClusterName: s.ClusterName(),
Lifecycle: infrav1.ResourceLifecycleOwned,
Name: to.StringPtr(s.ResourceGroup()),
Role: to.StringPtr(infrav1.CommonRole),
Additional: s.AdditionalTags(),
}),
Annotation: infrav1.RGTagsLastAppliedAnnotation,
},
}
}
36 changes: 36 additions & 0 deletions azure/scope/managedcontrolplane.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package scope
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net"
"strings"
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion azure/services/groups/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type azureClient struct {

var _ client = (*azureClient)(nil)

// newClient creates a new VM client from subscription ID.
// newClient creates a new Resource Group client from subscription ID.
func newClient(auth azure.Authorizer) *azureClient {
c := newGroupsClient(auth.SubscriptionID(), auth.BaseURI(), auth.Authorizer())
return &azureClient{
Expand Down
1 change: 1 addition & 0 deletions azure/services/groups/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
4 changes: 2 additions & 2 deletions azure/services/tags/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
47 changes: 47 additions & 0 deletions controllers/azurecluster_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ package controllers
import (
"context"

"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
"github.com/pkg/errors"
clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"

"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/scope"
"sigs.k8s.io/cluster-api-provider-azure/azure/services/bastionhosts"
"sigs.k8s.io/cluster-api-provider-azure/azure/services/groups"
Expand All @@ -34,6 +36,7 @@ import (
"sigs.k8s.io/cluster-api-provider-azure/azure/services/routetables"
"sigs.k8s.io/cluster-api-provider-azure/azure/services/securitygroups"
"sigs.k8s.io/cluster-api-provider-azure/azure/services/subnets"
"sigs.k8s.io/cluster-api-provider-azure/azure/services/tags"
"sigs.k8s.io/cluster-api-provider-azure/azure/services/virtualnetworks"
"sigs.k8s.io/cluster-api-provider-azure/util/tele"
)
Expand All @@ -52,6 +55,7 @@ type azureClusterService struct {
bastionSvc azure.Reconciler
skuCache *resourceskus.Cache
natGatewaySvc azure.Reconciler
tagsSvc azure.Reconciler
}

// newAzureClusterService populates all the services based on input scope.
Expand All @@ -74,6 +78,7 @@ func newAzureClusterService(scope *scope.ClusterScope) (*azureClusterService, er
privateDNSSvc: privatedns.New(scope),
bastionSvc: bastionhosts.New(scope),
skuCache: skuCache,
tagsSvc: tags.New(scope),
}, nil
}

Expand Down Expand Up @@ -131,9 +136,51 @@ func (s *azureClusterService) Reconcile(ctx context.Context) error {
return errors.Wrap(err, "failed to reconcile bastion")
}

if err := s.reconcileTags(ctx); err != nil {
return errors.Wrap(err, "failed to reconcile resource tags")
}

return nil
}

func (s *azureClusterService) reconcileTags(ctx context.Context) error {
// check that the resource group is not BYO.
managed, err := s.isResourceGroupManaged(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
}

func (s *azureClusterService) isResourceGroupManaged(ctx context.Context) (bool, error) {
// group service IsGroupManaged method is not available as we use group service as a azure.Reconciler
// which restricts us to only two methods

// Get resource group client - currently it's a non exported feature in groups package.
// Below is duplicated code without telemetry of azure API calls
groupsClient := resources.NewGroupsClientWithBaseURI(s.scope.BaseURI(), s.scope.SubscriptionID())
azure.SetAutoRestClientDefaults(&groupsClient.Client, s.scope.Authorizer())

// Get resource group
group, err := groupsClient.Get(ctx, s.scope.ResourceGroup())
if err != nil {
return false, err
}

// check if resource group is managed or not
tags := converters.MapToTags(group.Tags)
return tags.HasOwned(s.scope.ClusterName()), nil
}

// Delete reconciles all the services in a predetermined order.
func (s *azureClusterService) Delete(ctx context.Context) error {
ctx, span := tele.Tracer().Start(ctx, "controllers.azureClusterService.Delete")
Expand Down

0 comments on commit f8b14fa

Please sign in to comment.