Skip to content

Commit ec154f7

Browse files
committed
fix AKS out-of-band tag reconciliation
1 parent 7bc81c1 commit ec154f7

File tree

11 files changed

+132
-36
lines changed

11 files changed

+132
-36
lines changed

azure/const.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ const (
2929
// for annotation formatting rules.
3030
RGTagsLastAppliedAnnotation = "sigs.k8s.io/cluster-api-provider-azure-last-applied-tags-rg"
3131

32+
// ManagedClusterTagsLastAppliedAnnotation is the key for the AzureManagedControlPlane
33+
// object annotation which tracks the AdditionalTags for managed clusters.
34+
// See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
35+
// for annotation formatting rules.
36+
ManagedClusterTagsLastAppliedAnnotation = "sigs.k8s.io/cluster-api-provider-azure-last-applied-tags-managedcluster"
37+
3238
// CustomDataHashAnnotation is the key for the machine object annotation
3339
// which tracks the hash of the custom data.
3440
// See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/

azure/scope/managedcontrolplane.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,11 +422,12 @@ func (s *ManagedControlPlaneScope) ManagedClusterAnnotations() map[string]string
422422
}
423423

424424
// ManagedClusterSpec returns the managed cluster spec.
425-
func (s *ManagedControlPlaneScope) ManagedClusterSpec(ctx context.Context) azure.ResourceSpecGetter {
425+
func (s *ManagedControlPlaneScope) ManagedClusterSpec() azure.ResourceSpecGetter {
426426
managedClusterSpec := managedclusters.ManagedClusterSpec{
427427
Name: s.ControlPlane.Name,
428428
ResourceGroup: s.ControlPlane.Spec.ResourceGroupName,
429429
NodeResourceGroup: s.ControlPlane.Spec.NodeResourceGroupName,
430+
ClusterName: s.ClusterName(),
430431
Location: s.ControlPlane.Spec.Location,
431432
Tags: s.ControlPlane.Spec.AdditionalTags,
432433
Headers: maps.FilterByKeyPrefix(s.ManagedClusterAnnotations(), infrav1.CustomHeaderPrefix),
@@ -688,6 +689,11 @@ func (s *ManagedControlPlaneScope) TagsSpecs() []azure.TagsSpec {
688689
Tags: s.AdditionalTags(),
689690
Annotation: azure.RGTagsLastAppliedAnnotation,
690691
},
692+
{
693+
Scope: azure.ManagedClusterID(s.SubscriptionID(), s.ResourceGroup(), s.ManagedClusterSpec().ResourceName()),
694+
Tags: s.AdditionalTags(),
695+
Annotation: azure.ManagedClusterTagsLastAppliedAnnotation,
696+
},
691697
}
692698
}
693699

azure/scope/managedcontrolplane_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func TestManagedControlPlaneScope_OutboundType(t *testing.T) {
8888
c.Input.Client = fakeClient
8989
s, err := NewManagedControlPlaneScope(context.TODO(), c.Input)
9090
g.Expect(err).To(Succeed())
91-
managedCluster := s.ManagedClusterSpec(context.TODO())
91+
managedCluster := s.ManagedClusterSpec()
9292
result := managedCluster.(*managedclusters.ManagedClusterSpec).OutboundType == nil
9393
g.Expect(result).To(Equal(c.Expected))
9494
})
@@ -326,7 +326,7 @@ func TestManagedControlPlaneScope_AddonProfiles(t *testing.T) {
326326
c.Input.Client = fakeClient
327327
s, err := NewManagedControlPlaneScope(context.TODO(), c.Input)
328328
g.Expect(err).To(Succeed())
329-
managedCluster := s.ManagedClusterSpec(context.TODO())
329+
managedCluster := s.ManagedClusterSpec()
330330
g.Expect(managedCluster.(*managedclusters.ManagedClusterSpec).AddonProfiles).To(Equal(c.Expected))
331331
})
332332
}

azure/services/managedclusters/managedclusters.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const serviceName = "managedcluster"
3737
type ManagedClusterScope interface {
3838
azure.Authorizer
3939
azure.AsyncStatusUpdater
40-
ManagedClusterSpec(context.Context) azure.ResourceSpecGetter
40+
ManagedClusterSpec() azure.ResourceSpecGetter
4141
SetControlPlaneEndpoint(clusterv1.APIEndpoint)
4242
MakeEmptyKubeConfigSecret() corev1.Secret
4343
GetKubeConfigData() []byte
@@ -74,7 +74,7 @@ func (s *Service) Reconcile(ctx context.Context) error {
7474
ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureServiceReconcileTimeout)
7575
defer cancel()
7676

77-
managedClusterSpec := s.Scope.ManagedClusterSpec(ctx)
77+
managedClusterSpec := s.Scope.ManagedClusterSpec()
7878
if managedClusterSpec == nil {
7979
return nil
8080
}
@@ -112,7 +112,7 @@ func (s *Service) Delete(ctx context.Context) error {
112112
ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureServiceReconcileTimeout)
113113
defer cancel()
114114

115-
managedClusterSpec := s.Scope.ManagedClusterSpec(ctx)
115+
managedClusterSpec := s.Scope.ManagedClusterSpec()
116116
if managedClusterSpec == nil {
117117
return nil
118118
}

azure/services/managedclusters/managedclusters_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ func TestReconcile(t *testing.T) {
4444
name: "noop if managedcluster spec is nil",
4545
expectedError: "",
4646
expect: func(m *mock_managedclusters.MockCredentialGetterMockRecorder, s *mock_managedclusters.MockManagedClusterScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
47-
s.ManagedClusterSpec(gomockinternal.AContext()).Return(nil)
47+
s.ManagedClusterSpec().Return(nil)
4848
},
4949
},
5050
{
5151
name: "create managed cluster returns an error",
5252
expectedError: "some unexpected error occurred",
5353
expect: func(m *mock_managedclusters.MockCredentialGetterMockRecorder, s *mock_managedclusters.MockManagedClusterScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
54-
s.ManagedClusterSpec(gomockinternal.AContext()).Return(fakeManagedClusterSpec)
54+
s.ManagedClusterSpec().Return(fakeManagedClusterSpec)
5555
r.CreateOrUpdateResource(gomockinternal.AContext(), fakeManagedClusterSpec, serviceName).Return(nil, errors.New("some unexpected error occurred"))
5656
s.UpdatePutStatus(infrav1.ManagedClusterRunningCondition, serviceName, errors.New("some unexpected error occurred"))
5757
},
@@ -60,7 +60,7 @@ func TestReconcile(t *testing.T) {
6060
name: "create managed cluster succeeds",
6161
expectedError: "",
6262
expect: func(m *mock_managedclusters.MockCredentialGetterMockRecorder, s *mock_managedclusters.MockManagedClusterScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
63-
s.ManagedClusterSpec(gomockinternal.AContext()).Return(fakeManagedClusterSpec)
63+
s.ManagedClusterSpec().Return(fakeManagedClusterSpec)
6464
r.CreateOrUpdateResource(gomockinternal.AContext(), fakeManagedClusterSpec, serviceName).Return(containerservice.ManagedCluster{
6565
ManagedClusterProperties: &containerservice.ManagedClusterProperties{
6666
Fqdn: pointer.String("my-managedcluster-fqdn"),
@@ -80,7 +80,7 @@ func TestReconcile(t *testing.T) {
8080
name: "fail to get managed cluster credentials",
8181
expectedError: "failed to get credentials for managed cluster: internal server error",
8282
expect: func(m *mock_managedclusters.MockCredentialGetterMockRecorder, s *mock_managedclusters.MockManagedClusterScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
83-
s.ManagedClusterSpec(gomockinternal.AContext()).Return(fakeManagedClusterSpec)
83+
s.ManagedClusterSpec().Return(fakeManagedClusterSpec)
8484
r.CreateOrUpdateResource(gomockinternal.AContext(), fakeManagedClusterSpec, serviceName).Return(containerservice.ManagedCluster{
8585
ManagedClusterProperties: &containerservice.ManagedClusterProperties{
8686
Fqdn: pointer.String("my-managedcluster-fqdn"),
@@ -136,14 +136,14 @@ func TestDelete(t *testing.T) {
136136
name: "noop if no managed cluster spec is found",
137137
expectedError: "",
138138
expect: func(s *mock_managedclusters.MockManagedClusterScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
139-
s.ManagedClusterSpec(gomockinternal.AContext()).Return(nil)
139+
s.ManagedClusterSpec().Return(nil)
140140
},
141141
},
142142
{
143143
name: "successfully delete an existing managed cluster",
144144
expectedError: "",
145145
expect: func(s *mock_managedclusters.MockManagedClusterScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
146-
s.ManagedClusterSpec(gomockinternal.AContext()).Return(fakeManagedClusterSpec)
146+
s.ManagedClusterSpec().Return(fakeManagedClusterSpec)
147147
r.DeleteResource(gomockinternal.AContext(), fakeManagedClusterSpec, serviceName).Return(nil)
148148
s.UpdateDeleteStatus(infrav1.ManagedClusterRunningCondition, serviceName, nil)
149149
},
@@ -152,7 +152,7 @@ func TestDelete(t *testing.T) {
152152
name: "managed cluster deletion fails",
153153
expectedError: "internal error",
154154
expect: func(s *mock_managedclusters.MockManagedClusterScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
155-
s.ManagedClusterSpec(gomockinternal.AContext()).Return(fakeManagedClusterSpec)
155+
s.ManagedClusterSpec().Return(fakeManagedClusterSpec)
156156
r.DeleteResource(gomockinternal.AContext(), fakeManagedClusterSpec, serviceName).Return(errors.New("internal error"))
157157
s.UpdateDeleteStatus(infrav1.ManagedClusterRunningCondition, serviceName, errors.New("internal error"))
158158
},

azure/services/managedclusters/mock_managedclusters/managedclusters_mock.go

Lines changed: 4 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

azure/services/managedclusters/spec.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ type ManagedClusterSpec struct {
4444
// NodeResourceGroup is the name of the Azure resource group containing IaaS VMs.
4545
NodeResourceGroup string
4646

47+
// ClusterName is the name of the owning Cluster API Cluster resource.
48+
ClusterName string
49+
4750
// VnetSubnetID is the Azure Resource ID for the subnet which should contain nodes.
4851
VnetSubnetID string
4952

@@ -270,7 +273,13 @@ func (s *ManagedClusterSpec) Parameters(ctx context.Context, existing interface{
270273
Type: containerservice.ResourceIdentityTypeSystemAssigned,
271274
},
272275
Location: &s.Location,
273-
Tags: *azure.StringMapPtr(s.Tags),
276+
Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{
277+
Lifecycle: infrav1.ResourceLifecycleOwned,
278+
ClusterName: s.ClusterName,
279+
Name: pointer.String(s.Name),
280+
Role: pointer.String(infrav1.CommonRole),
281+
Additional: s.Tags,
282+
})),
274283
ManagedClusterProperties: &containerservice.ManagedClusterProperties{
275284
NodeResourceGroup: &s.NodeResourceGroup,
276285
EnableRBAC: pointer.Bool(true),
@@ -410,12 +419,6 @@ func (s *ManagedClusterSpec) Parameters(ctx context.Context, existing interface{
410419
// AgentPool changes are managed through AMMP.
411420
managedCluster.AgentPoolProfiles = existingMC.AgentPoolProfiles
412421

413-
// Do not trigger an update because of nil/empty discrepancies between the two sets of tags.
414-
if len(existingMC.Tags) == 0 && len(managedCluster.Tags) == 0 {
415-
existingMC.Tags = nil
416-
managedCluster.Tags = nil
417-
}
418-
419422
diff := computeDiffOfNormalizedClusters(managedCluster, existingMC)
420423
if diff == "" {
421424
log.V(4).Info("no changes found between user-updated spec and existing spec")
@@ -563,11 +566,9 @@ func computeDiffOfNormalizedClusters(managedCluster containerservice.ManagedClus
563566

564567
clusterNormalized := &containerservice.ManagedCluster{
565568
ManagedClusterProperties: propertiesNormalized,
566-
Tags: managedCluster.Tags,
567569
}
568570
existingMCClusterNormalized := &containerservice.ManagedCluster{
569571
ManagedClusterProperties: existingMCPropertiesNormalized,
570-
Tags: existingMC.Tags,
571572
}
572573

573574
if managedCluster.Sku != nil {

azure/services/managedclusters/spec_test.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"k8s.io/utils/pointer"
2727
infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
2828
"sigs.k8s.io/cluster-api-provider-azure/azure"
29+
"sigs.k8s.io/cluster-api-provider-azure/azure/converters"
2930
"sigs.k8s.io/cluster-api-provider-azure/azure/services/agentpools"
3031
gomockinternal "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock"
3132
)
@@ -60,6 +61,7 @@ func TestParameters(t *testing.T) {
6061
Name: "test-managedcluster",
6162
ResourceGroup: "test-rg",
6263
NodeResourceGroup: "test-node-rg",
64+
ClusterName: "test-cluster",
6365
Location: "test-location",
6466
Tags: map[string]string{
6567
"test-tag": "test-value",
@@ -139,13 +141,17 @@ func TestParameters(t *testing.T) {
139141
name: "delete all tags",
140142
existing: getExistingCluster(),
141143
spec: &ManagedClusterSpec{
142-
Tags: nil,
144+
Name: "test-managedcluster",
145+
ResourceGroup: "test-rg",
146+
Location: "test-location",
147+
Tags: nil,
148+
Version: "v1.22.0",
149+
LoadBalancerSKU: "Standard",
143150
},
144151
expect: func(g *WithT, result interface{}) {
145-
g.Expect(result).To(BeAssignableToTypeOf(containerservice.ManagedCluster{}))
146-
tags := result.(containerservice.ManagedCluster).Tags
147-
g.Expect(tags).NotTo(BeNil())
148-
g.Expect(tags).To(BeEmpty())
152+
// Additional tags are handled by azure/services/tags, so a diff
153+
// here shouldn't trigger an update on the managed cluster resource.
154+
g.Expect(result).To(BeNil())
149155
},
150156
},
151157
}
@@ -227,8 +233,14 @@ func getSampleManagedCluster() containerservice.ManagedCluster {
227233
Type: containerservice.ResourceIdentityTypeSystemAssigned,
228234
},
229235
Location: pointer.String("test-location"),
230-
Tags: map[string]*string{
231-
"test-tag": pointer.String("test-value"),
232-
},
236+
Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{
237+
Lifecycle: infrav1.ResourceLifecycleOwned,
238+
ClusterName: "test-cluster",
239+
Name: pointer.String("test-managedcluster"),
240+
Role: pointer.String(infrav1.CommonRole),
241+
Additional: infrav1.Tags{
242+
"test-tag": "test-value",
243+
},
244+
})),
233245
}
234246
}

azure/services/tags/tags.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ func (s *Service) Name() string {
5757
return serviceName
5858
}
5959

60+
// Some resource types are always assumed to be managed by CAPZ whether or not
61+
// they have the canonical "owned" tag applied to most resources. The annotation
62+
// key for those types should be listed here so their tags are always
63+
// interpreted as managed.
64+
var alwaysManagedAnnotations = map[string]struct{}{
65+
azure.ManagedClusterTagsLastAppliedAnnotation: {},
66+
}
67+
6068
// Reconcile ensures tags are correct.
6169
func (s *Service) Reconcile(ctx context.Context) error {
6270
ctx, log, done := tele.StartSpanWithLogger(ctx, "tags.Service.Reconcile")
@@ -72,7 +80,7 @@ func (s *Service) Reconcile(ctx context.Context) error {
7280
tags = existingTags.Properties.Tags
7381
}
7482

75-
if !s.isResourceManaged(tags) {
83+
if _, alwaysManaged := alwaysManagedAnnotations[tagsSpec.Annotation]; !alwaysManaged && !s.isResourceManaged(tags) {
7684
log.V(4).Info("Skipping tags reconcile for not managed resource")
7785
continue
7886
}

azure/services/tags/tags_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,38 @@ func TestReconcileTags(t *testing.T) {
114114
m.GetAtScope(gomockinternal.AContext(), "/sub/123/fake/scope").Return(resources.TagsResource{}, nil)
115115
},
116116
},
117+
{
118+
name: "create tags for managed resource without \"owned\" tag",
119+
expectedError: "",
120+
expect: func(s *mock_tags.MockTagScopeMockRecorder, m *mock_tags.MockclientMockRecorder) {
121+
annotation := azure.ManagedClusterTagsLastAppliedAnnotation
122+
gomock.InOrder(
123+
s.ClusterName().AnyTimes().Return("test-cluster"),
124+
s.TagsSpecs().Return([]azure.TagsSpec{
125+
{
126+
Scope: "/sub/123/fake/scope",
127+
Tags: map[string]string{
128+
"foo": "bar",
129+
"thing": "stuff",
130+
},
131+
Annotation: annotation,
132+
},
133+
}),
134+
m.GetAtScope(gomockinternal.AContext(), "/sub/123/fake/scope").Return(resources.TagsResource{}, nil),
135+
s.AnnotationJSON(annotation),
136+
m.UpdateAtScope(gomockinternal.AContext(), "/sub/123/fake/scope", resources.TagsPatchResource{
137+
Operation: "Merge",
138+
Properties: &resources.Tags{
139+
Tags: map[string]*string{
140+
"foo": pointer.String("bar"),
141+
"thing": pointer.String("stuff"),
142+
},
143+
},
144+
}),
145+
s.UpdateAnnotationJSON(annotation, map[string]interface{}{"foo": "bar", "thing": "stuff"}),
146+
)
147+
},
148+
},
117149
{
118150
name: "delete removed tags",
119151
expectedError: "",

0 commit comments

Comments
 (0)