Skip to content

Commit f2bbf36

Browse files
authored
Merge pull request #3134 from newrelic-forks/customData_Model_Fix
Custom data model fix
2 parents c1ef998 + 9e26a5e commit f2bbf36

File tree

9 files changed

+182
-2
lines changed

9 files changed

+182
-2
lines changed

azure/const.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,10 @@ const (
2828
// See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
2929
// for annotation formatting rules.
3030
RGTagsLastAppliedAnnotation = "sigs.k8s.io/cluster-api-provider-azure-last-applied-tags-rg"
31+
32+
// CustomDataHashAnnotation is the key for the machine object annotation
33+
// which tracks the hash of the custom data.
34+
// See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
35+
// for annotation formatting rules.
36+
CustomDataHashAnnotation = "sigs.k8s.io/cluster-api-provider-azure-vmss-custom-data-hash"
3137
)

azure/scope/machinepool.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ package scope
1818

1919
import (
2020
"context"
21+
"crypto/sha256"
2122
"encoding/base64"
2223
"fmt"
24+
"io"
2325
"strings"
2426

2527
azureautorest "github.com/Azure/go-autorest/autorest/azure"
@@ -535,6 +537,12 @@ func (m *MachinePoolScope) Close(ctx context.Context) error {
535537
if err := m.updateReplicasAndProviderIDs(ctx); err != nil {
536538
return errors.Wrap(err, "failed to update replicas and providerIDs")
537539
}
540+
if m.HasReplicasExternallyManaged(ctx) {
541+
if err := m.updateCustomDataHash(ctx); err != nil {
542+
// ignore errors to calculating the custom data hash since it's not absolutely crucial.
543+
log.V(4).Error(err, "unable to update custom data hash, ignoring.")
544+
}
545+
}
538546
}
539547

540548
if err := m.PatchObject(ctx); err != nil {
@@ -568,6 +576,39 @@ func (m *MachinePoolScope) GetBootstrapData(ctx context.Context) (string, error)
568576
return base64.StdEncoding.EncodeToString(value), nil
569577
}
570578

579+
// calculateBootstrapDataHash calculates the sha256 hash of the bootstrap data.
580+
func (m *MachinePoolScope) calculateBootstrapDataHash(ctx context.Context) (string, error) {
581+
bootstrapData, err := m.GetBootstrapData(ctx)
582+
if err != nil {
583+
return "", err
584+
}
585+
h := sha256.New()
586+
n, err := io.WriteString(h, bootstrapData)
587+
if err != nil || n == 0 {
588+
return "", fmt.Errorf("unable to write custom data (bytes written: %q): %w", n, err)
589+
}
590+
return fmt.Sprintf("%x", h.Sum(nil)), nil
591+
}
592+
593+
// HasBootstrapDataChanges calculates the sha256 hash of the bootstrap data and compares it with the saved hash in AzureMachinePool.Status.
594+
func (m *MachinePoolScope) HasBootstrapDataChanges(ctx context.Context) (bool, error) {
595+
newHash, err := m.calculateBootstrapDataHash(ctx)
596+
if err != nil {
597+
return false, err
598+
}
599+
return m.AzureMachinePool.GetAnnotations()[azure.CustomDataHashAnnotation] != newHash, nil
600+
}
601+
602+
// updateCustomDataHash calculates the sha256 hash of the bootstrap data and saves it in AzureMachinePool.Status.
603+
func (m *MachinePoolScope) updateCustomDataHash(ctx context.Context) error {
604+
newHash, err := m.calculateBootstrapDataHash(ctx)
605+
if err != nil {
606+
return err
607+
}
608+
m.SetAnnotation(azure.CustomDataHashAnnotation, newHash)
609+
return nil
610+
}
611+
571612
// GetVMImage picks an image from the AzureMachinePool configuration, or uses a default one.
572613
func (m *MachinePoolScope) GetVMImage(ctx context.Context) (*infrav1.Image, error) {
573614
ctx, log, done := tele.StartSpanWithLogger(ctx, "scope.MachinePoolScope.GetVMImage")

azure/services/scalesets/mock_scalesets/scalesets_mock.go

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

azure/services/scalesets/scalesets.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ type (
5353
SetProviderID(string)
5454
SetVMSSState(*azure.VMSS)
5555
ReconcileReplicas(context.Context, *azure.VMSS) error
56+
HasReplicasExternallyManaged(context.Context) bool
57+
HasBootstrapDataChanges(context.Context) (bool, error)
5658
}
5759

5860
// Service provides operations on Azure resources.
@@ -276,6 +278,20 @@ func (s *Service) patchVMSSIfNeeded(ctx context.Context, infraVMSS *azure.VMSS)
276278
return nil, errors.Wrap(err, "failed to calculate maxSurge")
277279
}
278280

281+
// If the VMSS is managed by an external autoscaler, we should patch the VMSS if customData has changed.
282+
shouldPatchCustomData := false
283+
if s.Scope.HasReplicasExternallyManaged(ctx) {
284+
shouldPatchCustomData, err := s.Scope.HasBootstrapDataChanges(ctx)
285+
if err != nil {
286+
return nil, errors.Wrap(err, "unable to calculate custom data hash")
287+
}
288+
if shouldPatchCustomData {
289+
log.V(4).Info("custom data changed")
290+
} else {
291+
log.V(4).Info("custom data unchanged")
292+
}
293+
}
294+
279295
hasModelChanges := hasModelModifyingDifferences(infraVMSS, vmss)
280296
isFlex := s.Scope.ScaleSetSpec().OrchestrationMode == infrav1.FlexibleOrchestrationMode
281297
updated := true
@@ -289,10 +305,11 @@ func (s *Service) patchVMSSIfNeeded(ctx context.Context, infraVMSS *azure.VMSS)
289305
patch.Sku.Capacity = pointer.Int64(surge)
290306
}
291307

308+
// If the VMSS is managed by an external autoscaler, we should patch the VMSS if customData has changed.
292309
// If there are no model changes and no increase in the replica count, do not update the VMSS.
293310
// Decreases in replica count is handled by deleting AzureMachinePoolMachine instances in the MachinePoolScope
294-
if *patch.Sku.Capacity <= infraVMSS.Capacity && !hasModelChanges {
295-
log.V(4).Info("nothing to update on vmss", "scale set", spec.Name, "newReplicas", *patch.Sku.Capacity, "oldReplicas", infraVMSS.Capacity, "hasChanges", hasModelChanges)
311+
if *patch.Sku.Capacity <= infraVMSS.Capacity && !hasModelChanges && !shouldPatchCustomData {
312+
log.V(4).Info("nothing to update on vmss", "scale set", spec.Name, "newReplicas", *patch.Sku.Capacity, "oldReplicas", infraVMSS.Capacity, "hasModelChanges", hasModelChanges, "shouldPatchCustomData", shouldPatchCustomData)
296313
return nil, nil
297314
}
298315

azure/services/scalesets/scalesets_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ func TestReconcileVMSS(t *testing.T) {
223223
s.DeleteLongRunningOperationState(defaultSpec.Name, serviceName, infrav1.PutFuture)
224224
s.DeleteLongRunningOperationState(defaultSpec.Name, serviceName, infrav1.PatchFuture)
225225
s.UpdatePutStatus(infrav1.BootstrapSucceededCondition, serviceName, nil)
226+
s.HasReplicasExternallyManaged(gomockinternal.AContext()).Return(false)
226227
},
227228
},
228229
{
@@ -238,6 +239,7 @@ func TestReconcileVMSS(t *testing.T) {
238239
s.DeleteLongRunningOperationState(defaultSpec.Name, serviceName, infrav1.PutFuture)
239240
s.DeleteLongRunningOperationState(defaultSpec.Name, serviceName, infrav1.PatchFuture)
240241
s.UpdatePutStatus(infrav1.BootstrapSucceededCondition, serviceName, nil)
242+
s.HasReplicasExternallyManaged(gomockinternal.AContext()).Return(false)
241243
},
242244
},
243245
{
@@ -582,6 +584,7 @@ func TestReconcileVMSS(t *testing.T) {
582584
m.GetResultIfDone(gomockinternal.AContext(), patchFuture).Return(compute.VirtualMachineScaleSet{}, azure.NewOperationNotDoneError(patchFuture))
583585
m.Get(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName).Return(clone, nil)
584586
m.ListInstances(gomockinternal.AContext(), defaultResourceGroup, defaultVMSSName).Return(instances, nil)
587+
s.HasReplicasExternallyManaged(gomockinternal.AContext()).Return(false)
585588
},
586589
},
587590
{
@@ -741,6 +744,7 @@ func TestReconcileVMSS(t *testing.T) {
741744
s.DeleteLongRunningOperationState(spec.Name, serviceName, infrav1.PatchFuture)
742745
s.UpdatePutStatus(infrav1.BootstrapSucceededCondition, serviceName, nil)
743746
s.Location().AnyTimes().Return("test-location")
747+
s.HasReplicasExternallyManaged(gomockinternal.AContext()).Return(false)
744748
},
745749
},
746750
{
@@ -767,6 +771,7 @@ func TestReconcileVMSS(t *testing.T) {
767771
s.DeleteLongRunningOperationState(spec.Name, serviceName, infrav1.PatchFuture)
768772
s.UpdatePutStatus(infrav1.BootstrapSucceededCondition, serviceName, nil)
769773
s.Location().AnyTimes().Return("test-location")
774+
s.HasReplicasExternallyManaged(gomockinternal.AContext()).Return(false)
770775
},
771776
},
772777
{
@@ -792,6 +797,7 @@ func TestReconcileVMSS(t *testing.T) {
792797
s.DeleteLongRunningOperationState(spec.Name, serviceName, infrav1.PatchFuture)
793798
s.UpdatePutStatus(infrav1.BootstrapSucceededCondition, serviceName, nil)
794799
s.Location().AnyTimes().Return("test-location")
800+
s.HasReplicasExternallyManaged(gomockinternal.AContext()).Return(false)
795801
},
796802
},
797803
}

config/rbac/role.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ rules:
5252
- get
5353
- list
5454
- watch
55+
- apiGroups:
56+
- bootstrap.cluster.x-k8s.io
57+
resources:
58+
- kubeadmconfigs
59+
- kubeadmconfigs/status
60+
verbs:
61+
- get
62+
- list
63+
- watch
5564
- apiGroups:
5665
- cluster.x-k8s.io
5766
resources:

exp/controllers/azuremachinepool_controller.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,17 @@ import (
3434
"sigs.k8s.io/cluster-api-provider-azure/util/reconciler"
3535
"sigs.k8s.io/cluster-api-provider-azure/util/tele"
3636
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
37+
kubeadmv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
3738
expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
3839
"sigs.k8s.io/cluster-api/util"
3940
"sigs.k8s.io/cluster-api/util/annotations"
4041
"sigs.k8s.io/cluster-api/util/predicates"
4142
ctrl "sigs.k8s.io/controller-runtime"
43+
"sigs.k8s.io/controller-runtime/pkg/builder"
4244
"sigs.k8s.io/controller-runtime/pkg/client"
4345
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
4446
"sigs.k8s.io/controller-runtime/pkg/handler"
47+
"sigs.k8s.io/controller-runtime/pkg/predicate"
4548
"sigs.k8s.io/controller-runtime/pkg/reconcile"
4649
"sigs.k8s.io/controller-runtime/pkg/source"
4750
)
@@ -113,6 +116,12 @@ func (ampr *AzureMachinePoolReconciler) SetupWithManager(ctx context.Context, mg
113116
&source.Kind{Type: &infrav1.AzureCluster{}},
114117
handler.EnqueueRequestsFromMapFunc(azureClusterMapper),
115118
).
119+
// watch for changes in KubeadmConfig to sync bootstrap token
120+
Watches(
121+
&source.Kind{Type: &kubeadmv1.KubeadmConfig{}},
122+
handler.EnqueueRequestsFromMapFunc(KubeadmConfigToInfrastructureMapFunc(ctx, ampr.Client, log)),
123+
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
124+
).
116125
Build(r)
117126
if err != nil {
118127
return errors.Wrap(err, "error creating controller")
@@ -147,6 +156,7 @@ func (ampr *AzureMachinePoolReconciler) SetupWithManager(ctx context.Context, mg
147156

148157
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=azuremachinepools,verbs=get;list;watch;create;update;patch;delete
149158
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=azuremachinepools/status,verbs=get;update;patch
159+
// +kubebuilder:rbac:groups=bootstrap.cluster.x-k8s.io,resources=kubeadmconfigs;kubeadmconfigs/status,verbs=get;list;watch
150160
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=azuremachinepoolmachines,verbs=get;list;watch;create;update;patch;delete
151161
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=azuremachinepoolmachines/status,verbs=get
152162
// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinepools;machinepools/status,verbs=get;list;watch;update;patch

exp/controllers/helpers.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1beta1"
3333
"sigs.k8s.io/cluster-api-provider-azure/util/reconciler"
3434
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
35+
kubeadmv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
3536
expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
3637
"sigs.k8s.io/cluster-api/util"
3738
ctrl "sigs.k8s.io/controller-runtime"
@@ -317,3 +318,62 @@ func MachinePoolMachineHasStateOrVersionChange(logger logr.Logger) predicate.Fun
317318
GenericFunc: func(e event.GenericEvent) bool { return false },
318319
}
319320
}
321+
322+
// KubeadmConfigToInfrastructureMapFunc returns a handler.ToRequestsFunc that watches for KubeadmConfig events and returns.
323+
func KubeadmConfigToInfrastructureMapFunc(ctx context.Context, c client.Client, log logr.Logger) handler.MapFunc {
324+
return func(o client.Object) []reconcile.Request {
325+
ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultMappingTimeout)
326+
defer cancel()
327+
328+
kc, ok := o.(*kubeadmv1.KubeadmConfig)
329+
if !ok {
330+
log.V(4).Info("attempt to map incorrect type", "type", fmt.Sprintf("%T", o))
331+
return nil
332+
}
333+
334+
mpKey := client.ObjectKey{
335+
Namespace: kc.Namespace,
336+
Name: kc.Name,
337+
}
338+
339+
// fetch MachinePool to get reference
340+
mp := &expv1.MachinePool{}
341+
if err := c.Get(ctx, mpKey, mp); err != nil {
342+
if !apierrors.IsNotFound(err) {
343+
log.Error(err, "failed to fetch MachinePool for KubeadmConfig")
344+
}
345+
return []reconcile.Request{}
346+
}
347+
348+
ref := mp.Spec.Template.Spec.Bootstrap.ConfigRef
349+
if ref == nil {
350+
log.V(4).Info("fetched MachinePool has no Bootstrap.ConfigRef")
351+
return []reconcile.Request{}
352+
}
353+
sameKind := ref.Kind != o.GetObjectKind().GroupVersionKind().Kind
354+
sameName := ref.Name == kc.Name
355+
sameNamespace := ref.Namespace == kc.Namespace
356+
if !sameKind || !sameName || !sameNamespace {
357+
log.V(4).Info("Bootstrap.ConfigRef does not match",
358+
"sameKind", sameKind,
359+
"ref kind", ref.Kind,
360+
"other kind", o.GetObjectKind().GroupVersionKind().Kind,
361+
"sameName", sameName,
362+
"sameNamespace", sameNamespace,
363+
)
364+
return []reconcile.Request{}
365+
}
366+
367+
key := client.ObjectKey{
368+
Namespace: kc.Namespace,
369+
Name: kc.Name,
370+
}
371+
log.V(4).Info("adding KubeadmConfig to watch", "key", key)
372+
373+
return []reconcile.Request{
374+
{
375+
NamespacedName: key,
376+
},
377+
}
378+
}
379+
}

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import (
5252
"sigs.k8s.io/cluster-api-provider-azure/version"
5353
clusterv1alpha4 "sigs.k8s.io/cluster-api/api/v1alpha4"
5454
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
55+
kubeadmv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
5556
expv1alpha4 "sigs.k8s.io/cluster-api/exp/api/v1alpha4"
5657
expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
5758
capifeature "sigs.k8s.io/cluster-api/feature"
@@ -80,6 +81,7 @@ func init() {
8081
_ = expv1alpha4.AddToScheme(scheme)
8182
_ = clusterv1.AddToScheme(scheme)
8283
_ = expv1.AddToScheme(scheme)
84+
_ = kubeadmv1.AddToScheme(scheme)
8385
// +kubebuilder:scaffold:scheme
8486

8587
// Add aadpodidentity v1 to the scheme.

0 commit comments

Comments
 (0)