Skip to content

Commit 1d29cae

Browse files
authored
Merge pull request #4777 from nojnhuh/v2-mpnodes
ASOAPI: reconcile nodes for AzureASOManagedMachinePool
2 parents 6d229b4 + b24e0db commit 1d29cae

7 files changed

+232
-2
lines changed

config/crd/bases/infrastructure.cluster.x-k8s.io_azureasomanagedmachinepools.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ spec:
4141
description: AzureASOManagedMachinePoolSpec defines the desired state
4242
of AzureASOManagedMachinePool.
4343
properties:
44+
providerIDList:
45+
description: |-
46+
ProviderIDList is the list of cloud provider IDs for the instances. It fulfills Cluster API's machine
47+
pool infrastructure provider contract.
48+
items:
49+
type: string
50+
type: array
4451
resources:
4552
description: Resources are embedded ASO resources to be managed by
4653
this resource.
@@ -53,6 +60,12 @@ spec:
5360
description: AzureASOManagedMachinePoolStatus defines the observed state
5461
of AzureASOManagedMachinePool.
5562
properties:
63+
replicas:
64+
description: |-
65+
Replicas is the current number of provisioned replicas. It fulfills Cluster API's machine pool
66+
infrastructure provider contract.
67+
format: int32
68+
type: integer
5669
resources:
5770
items:
5871
description: ResourceStatus represents the status of a resource.

exp/api/v1alpha1/azureasomanagedmachinepool_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ type AzureASOManagedMachinePoolSpec struct {
3030

3131
// AzureASOManagedMachinePoolStatus defines the observed state of AzureASOManagedMachinePool.
3232
type AzureASOManagedMachinePoolStatus struct {
33+
// Replicas is the current number of provisioned replicas. It fulfills Cluster API's machine pool
34+
// infrastructure provider contract.
35+
//+optional
36+
Replicas int32 `json:"replicas"`
37+
3338
//+optional
3439
Resources []ResourceStatus `json:"resources,omitempty"`
3540
}

exp/api/v1alpha1/azureasomanagedmachinepooltemplate_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ type AzureASOManagedMachinePoolResource struct {
3333

3434
// AzureASOManagedMachinePoolTemplateResourceSpec defines the desired state of the templated resource.
3535
type AzureASOManagedMachinePoolTemplateResourceSpec struct {
36+
// ProviderIDList is the list of cloud provider IDs for the instances. It fulfills Cluster API's machine
37+
// pool infrastructure provider contract.
38+
ProviderIDList []string `json:"providerIDList,omitempty"`
39+
3640
// Resources are embedded ASO resources to be managed by this resource.
3741
//+optional
3842
Resources []runtime.RawExtension `json:"resources,omitempty"`

exp/api/v1alpha1/zz_generated.deepcopy.go

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

exp/controllers/azureasomanagedmachinepool_controller.go

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,13 @@ package controllers
1919
import (
2020
"context"
2121
"fmt"
22+
"slices"
2223

24+
asocontainerservicev1 "github.com/Azure/azure-service-operator/v2/api/containerservice/v1api20231001"
25+
corev1 "k8s.io/api/core/v1"
2326
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
27+
"k8s.io/apimachinery/pkg/types"
28+
"k8s.io/utils/ptr"
2429
infracontroller "sigs.k8s.io/cluster-api-provider-azure/controllers"
2530
infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1alpha1"
2631
"sigs.k8s.io/cluster-api-provider-azure/util/tele"
@@ -44,10 +49,16 @@ import (
4449
type AzureASOManagedMachinePoolReconciler struct {
4550
client.Client
4651
WatchFilterValue string
52+
Tracker ClusterTracker
4753

4854
newResourceReconciler func(*infrav1exp.AzureASOManagedMachinePool, []*unstructured.Unstructured) resourceReconciler
4955
}
5056

57+
// ClusterTracker wraps a CAPI remote.ClusterCacheTracker.
58+
type ClusterTracker interface {
59+
GetClient(context.Context, types.NamespacedName) (client.Client, error)
60+
}
61+
5162
// SetupWithManager sets up the controller with the Manager.
5263
func (r *AzureASOManagedMachinePoolReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
5364
_, log, done := tele.StartSpanWithLogger(ctx,
@@ -184,7 +195,6 @@ func (r *AzureASOManagedMachinePoolReconciler) Reconcile(ctx context.Context, re
184195

185196
//nolint:unparam // these parameters will be used soon enough
186197
func (r *AzureASOManagedMachinePoolReconciler) reconcileNormal(ctx context.Context, asoManagedMachinePool *infrav1exp.AzureASOManagedMachinePool, machinePool *expv1.MachinePool, cluster *clusterv1.Cluster) (ctrl.Result, error) {
187-
//nolint:all // ctx will be used soon
188198
ctx, log, done := tele.StartSpanWithLogger(ctx,
189199
"controllers.AzureASOManagedMachinePoolReconciler.reconcileNormal",
190200
)
@@ -200,6 +210,19 @@ func (r *AzureASOManagedMachinePoolReconciler) reconcileNormal(ctx context.Conte
200210
if err != nil {
201211
return ctrl.Result{}, err
202212
}
213+
214+
var agentPoolName string
215+
for _, resource := range us {
216+
if resource.GroupVersionKind().Group == asocontainerservicev1.GroupVersion.Group &&
217+
resource.GroupVersionKind().Kind == "ManagedClustersAgentPool" {
218+
agentPoolName = resource.GetName()
219+
break
220+
}
221+
}
222+
if agentPoolName == "" {
223+
return ctrl.Result{}, reconcile.TerminalError(fmt.Errorf("no %s ManagedClustersAgentPools defined in AzureASOManagedMachinePool spec.resources", asocontainerservicev1.GroupVersion.Group))
224+
}
225+
203226
resourceReconciler := r.newResourceReconciler(asoManagedMachinePool, us)
204227
err = resourceReconciler.Reconcile(ctx)
205228
if err != nil {
@@ -211,9 +234,56 @@ func (r *AzureASOManagedMachinePoolReconciler) reconcileNormal(ctx context.Conte
211234
}
212235
}
213236

237+
agentPool := &asocontainerservicev1.ManagedClustersAgentPool{}
238+
err = r.Get(ctx, client.ObjectKey{Namespace: asoManagedMachinePool.Namespace, Name: agentPoolName}, agentPool)
239+
if err != nil {
240+
return ctrl.Result{}, fmt.Errorf("error getting ManagedClustersAgentPool: %w", err)
241+
}
242+
243+
managedCluster := &asocontainerservicev1.ManagedCluster{}
244+
err = r.Get(ctx, client.ObjectKey{Namespace: agentPool.Namespace, Name: agentPool.Owner().Name}, managedCluster)
245+
if err != nil {
246+
return ctrl.Result{}, fmt.Errorf("error getting ManagedCluster: %w", err)
247+
}
248+
if managedCluster.Status.NodeResourceGroup == nil {
249+
return ctrl.Result{}, nil
250+
}
251+
rg := *managedCluster.Status.NodeResourceGroup
252+
253+
clusterClient, err := r.Tracker.GetClient(ctx, util.ObjectKey(cluster))
254+
if err != nil {
255+
return ctrl.Result{}, err
256+
}
257+
nodes := &corev1.NodeList{}
258+
err = clusterClient.List(ctx, nodes,
259+
client.MatchingLabels(expectedNodeLabels(agentPool.AzureName(), rg)),
260+
)
261+
if err != nil {
262+
return ctrl.Result{}, fmt.Errorf("failed to list nodes in workload cluster: %w", err)
263+
}
264+
providerIDs := make([]string, 0, len(nodes.Items))
265+
for _, node := range nodes.Items {
266+
if node.Spec.ProviderID == "" {
267+
// the node will receive a provider id soon
268+
return ctrl.Result{Requeue: true}, nil
269+
}
270+
providerIDs = append(providerIDs, node.Spec.ProviderID)
271+
}
272+
// Prevent a different order from updating the spec.
273+
slices.Sort(providerIDs)
274+
asoManagedMachinePool.Spec.ProviderIDList = providerIDs
275+
asoManagedMachinePool.Status.Replicas = int32(ptr.Deref(agentPool.Status.Count, 0))
276+
214277
return ctrl.Result{}, nil
215278
}
216279

280+
func expectedNodeLabels(poolName, nodeRG string) map[string]string {
281+
return map[string]string{
282+
"kubernetes.azure.com/agentpool": poolName,
283+
"kubernetes.azure.com/cluster": nodeRG,
284+
}
285+
}
286+
217287
//nolint:unparam // these parameters will be used soon enough
218288
func (r *AzureASOManagedMachinePoolReconciler) reconcilePause(ctx context.Context, asoManagedMachinePool *infrav1exp.AzureASOManagedMachinePool, cluster *clusterv1.Cluster) (ctrl.Result, error) {
219289
//nolint:all // ctx will be used soon

exp/controllers/azureasomanagedmachinepool_controller_test.go

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@ package controllers
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"testing"
2223
"time"
2324

25+
asocontainerservicev1 "github.com/Azure/azure-service-operator/v2/api/containerservice/v1api20231001"
26+
"github.com/Azure/azure-service-operator/v2/pkg/genruntime"
2427
. "github.com/onsi/gomega"
2528
corev1 "k8s.io/api/core/v1"
2629
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2730
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2831
"k8s.io/apimachinery/pkg/runtime"
2932
"k8s.io/apimachinery/pkg/types"
33+
"k8s.io/utils/ptr"
3034
infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1alpha1"
3135
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3236
expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
@@ -35,6 +39,17 @@ import (
3539
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
3640
)
3741

42+
type FakeClusterTracker struct {
43+
getClientFunc func(context.Context, types.NamespacedName) (client.Client, error)
44+
}
45+
46+
func (c *FakeClusterTracker) GetClient(ctx context.Context, name types.NamespacedName) (client.Client, error) {
47+
if c.getClientFunc == nil {
48+
return nil, nil
49+
}
50+
return c.getClientFunc(ctx, name)
51+
}
52+
3853
func TestAzureASOManagedMachinePoolReconcile(t *testing.T) {
3954
ctx := context.Background()
4055

@@ -43,6 +58,7 @@ func TestAzureASOManagedMachinePoolReconcile(t *testing.T) {
4358
infrav1exp.AddToScheme,
4459
clusterv1.AddToScheme,
4560
expv1.AddToScheme,
61+
asocontainerservicev1.AddToScheme,
4662
)
4763
NewGomegaWithT(t).Expect(sb.AddToScheme(s)).To(Succeed())
4864
fakeClientBuilder := func() *fakeclient.ClientBuilder {
@@ -210,6 +226,19 @@ func TestAzureASOManagedMachinePoolReconcile(t *testing.T) {
210226
clusterv1.ClusterFinalizer,
211227
},
212228
},
229+
Spec: infrav1exp.AzureASOManagedMachinePoolSpec{
230+
AzureASOManagedMachinePoolTemplateResourceSpec: infrav1exp.AzureASOManagedMachinePoolTemplateResourceSpec{
231+
Resources: []runtime.RawExtension{
232+
{
233+
Raw: apJSON(g, &asocontainerservicev1.ManagedClustersAgentPool{
234+
ObjectMeta: metav1.ObjectMeta{
235+
Name: "ap",
236+
},
237+
}),
238+
},
239+
},
240+
},
241+
},
213242
}
214243
machinePool := &expv1.MachinePool{
215244
ObjectMeta: metav1.ObjectMeta{
@@ -259,6 +288,30 @@ func TestAzureASOManagedMachinePoolReconcile(t *testing.T) {
259288
},
260289
},
261290
}
291+
asoManagedCluster := &asocontainerservicev1.ManagedCluster{
292+
ObjectMeta: metav1.ObjectMeta{
293+
Name: "mc",
294+
Namespace: cluster.Namespace,
295+
},
296+
Status: asocontainerservicev1.ManagedCluster_STATUS{
297+
NodeResourceGroup: ptr.To("MC_rg"),
298+
},
299+
}
300+
asoAgentPool := &asocontainerservicev1.ManagedClustersAgentPool{
301+
ObjectMeta: metav1.ObjectMeta{
302+
Name: "ap",
303+
Namespace: cluster.Namespace,
304+
},
305+
Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
306+
AzureName: "pool1",
307+
Owner: &genruntime.KnownResourceReference{
308+
Name: asoManagedCluster.Name,
309+
},
310+
},
311+
Status: asocontainerservicev1.ManagedClusters_AgentPool_STATUS{
312+
Count: ptr.To(3),
313+
},
314+
}
262315
asoManagedMachinePool := &infrav1exp.AzureASOManagedMachinePool{
263316
ObjectMeta: metav1.ObjectMeta{
264317
Name: "ammp",
@@ -274,6 +327,15 @@ func TestAzureASOManagedMachinePoolReconcile(t *testing.T) {
274327
clusterv1.ClusterFinalizer,
275328
},
276329
},
330+
Spec: infrav1exp.AzureASOManagedMachinePoolSpec{
331+
AzureASOManagedMachinePoolTemplateResourceSpec: infrav1exp.AzureASOManagedMachinePoolTemplateResourceSpec{
332+
Resources: []runtime.RawExtension{
333+
{
334+
Raw: apJSON(g, asoAgentPool),
335+
},
336+
},
337+
},
338+
},
277339
}
278340
machinePool := &expv1.MachinePool{
279341
ObjectMeta: metav1.ObjectMeta{
@@ -285,7 +347,7 @@ func TestAzureASOManagedMachinePoolReconcile(t *testing.T) {
285347
},
286348
}
287349
c := fakeClientBuilder().
288-
WithObjects(asoManagedMachinePool, machinePool, cluster).
350+
WithObjects(asoManagedMachinePool, machinePool, cluster, asoAgentPool, asoManagedCluster).
289351
Build()
290352
r := &AzureASOManagedMachinePoolReconciler{
291353
Client: c,
@@ -296,10 +358,48 @@ func TestAzureASOManagedMachinePoolReconcile(t *testing.T) {
296358
},
297359
}
298360
},
361+
Tracker: &FakeClusterTracker{
362+
getClientFunc: func(_ context.Context, _ types.NamespacedName) (client.Client, error) {
363+
return fakeclient.NewClientBuilder().
364+
WithObjects(
365+
&corev1.Node{
366+
ObjectMeta: metav1.ObjectMeta{
367+
Name: "node1",
368+
Labels: expectedNodeLabels(asoAgentPool.AzureName(), *asoManagedCluster.Status.NodeResourceGroup),
369+
},
370+
Spec: corev1.NodeSpec{
371+
ProviderID: "azure://node1",
372+
},
373+
},
374+
&corev1.Node{
375+
ObjectMeta: metav1.ObjectMeta{
376+
Name: "node2",
377+
Labels: expectedNodeLabels(asoAgentPool.AzureName(), *asoManagedCluster.Status.NodeResourceGroup),
378+
},
379+
Spec: corev1.NodeSpec{
380+
ProviderID: "azure://node2",
381+
},
382+
},
383+
&corev1.Node{
384+
ObjectMeta: metav1.ObjectMeta{
385+
Name: "no-labels",
386+
},
387+
Spec: corev1.NodeSpec{
388+
ProviderID: "azure://node3",
389+
},
390+
},
391+
).
392+
Build(), nil
393+
},
394+
},
299395
}
300396
result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(asoManagedMachinePool)})
301397
g.Expect(err).NotTo(HaveOccurred())
302398
g.Expect(result).To(Equal(ctrl.Result{}))
399+
400+
g.Expect(r.Get(ctx, client.ObjectKeyFromObject(asoManagedMachinePool), asoManagedMachinePool)).To(Succeed())
401+
g.Expect(asoManagedMachinePool.Spec.ProviderIDList).To(ConsistOf("azure://node1", "azure://node2"))
402+
g.Expect(asoManagedMachinePool.Status.Replicas).To(Equal(int32(3)))
303403
})
304404

305405
t.Run("successfully reconciles pause", func(t *testing.T) {
@@ -410,3 +510,10 @@ func TestAzureASOManagedMachinePoolReconcile(t *testing.T) {
410510
g.Expect(result).To(Equal(ctrl.Result{}))
411511
})
412512
}
513+
514+
func apJSON(g Gomega, ap *asocontainerservicev1.ManagedClustersAgentPool) []byte {
515+
ap.SetGroupVersionKind(asocontainerservicev1.GroupVersion.WithKind("ManagedClustersAgentPool"))
516+
j, err := json.Marshal(ap)
517+
g.Expect(err).NotTo(HaveOccurred())
518+
return j
519+
}

0 commit comments

Comments
 (0)