Skip to content

Commit 0c64bea

Browse files
authored
Merge pull request #4799 from nojnhuh/v2-adopt
ASOAPI: basic automated adoption
2 parents 04c4e7e + 076bde4 commit 0c64bea

File tree

12 files changed

+514
-26
lines changed

12 files changed

+514
-26
lines changed

config/rbac/role.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ rules:
5454
- get
5555
- list
5656
- watch
57+
- apiGroups:
58+
- cluster.x-k8s.io
59+
resources:
60+
- clusters
61+
verbs:
62+
- create
5763
- apiGroups:
5864
- cluster.x-k8s.io
5965
resources:
@@ -64,6 +70,12 @@ rules:
6470
- list
6571
- patch
6672
- watch
73+
- apiGroups:
74+
- cluster.x-k8s.io
75+
resources:
76+
- machinepools
77+
verbs:
78+
- create
6779
- apiGroups:
6880
- cluster.x-k8s.io
6981
resources:

docs/book/src/topics/managedcluster.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,38 @@ Some notes about how this works under the hood:
629629

630630
## Adopting Existing AKS Clusters
631631

632+
### Option 1: Using the experimental ASO-based API
633+
634+
<!-- markdown-link-check-disable-next-line -->
635+
The [experimental AzureASOManagedControlPlane and related APIs](/topics/aso.html#experimental-aso-api) support
636+
adoption as a first-class use case. Going forward, this method is likely to be easier, more reliable, include
637+
more features, and better supported for adopting AKS clusters than Option 2 below.
638+
639+
To adopt an AKS cluster into a full Cluster API Cluster, create an ASO ManagedCluster and associated
640+
ManagedClustersAgentPool resources annotated with `sigs.k8s.io/cluster-api-provider-azure-adopt=true`. The
641+
annotation may also be added to existing ASO resources to trigger adoption. CAPZ will automatically scaffold
642+
the Cluster API resources like the Cluster, AzureASOManagedCluster, AzureASOManagedControlPlane, MachinePools,
643+
and AzureASOManagedMachinePools. The [`asoctl import
644+
azure-resource`](https://azure.github.io/azure-service-operator/tools/asoctl/#import-azure-resource) command
645+
can help generate the required YAML.
646+
647+
Caveats:
648+
- The `asoctl import azure-resource` command has at least [one known
649+
bug](https://github.com/Azure/azure-service-operator/issues/3805) requiring the YAML it generates to be
650+
edited before it can be applied to a cluster.
651+
- CAPZ currently only records the ASO resources in the CAPZ resources' `spec.resources` that it needs to
652+
function, which include the ManagedCluster, its ResourceGroup, and associated ManagedClustersAgentPools.
653+
Other resources owned by the ManagedCluster like Kubernetes extensions or Fleet memberships are not
654+
currently imported to the CAPZ specs.
655+
- Configuring the automatically generated Cluster API resources is not currently possible. If you need to
656+
change something like the `metadata.name` of a resource from what CAPZ generates, create the Cluster API
657+
resources manually referencing the pre-existing resources.
658+
- Adopting existing clusters created with the GA AzureManagedControlPlane API to the experimental API with
659+
this method is theoretically possible, but untested. Care should be taken to prevent CAPZ from reconciling
660+
two different representations of the same underlying Azure resources.
661+
662+
### Option 2: Using the current AzureManagedControlPlane API
663+
632664
<aside class="note">
633665

634666
<h1> Warning </h1>
@@ -665,7 +697,7 @@ Managed Cluster and Agent Pool resources do not need this tag in order to be ado
665697
After applying the CAPI and CAPZ resources for the cluster, other means of managing the cluster should be
666698
disabled to avoid ongoing conflicts with CAPZ's reconciliation process.
667699

668-
### Pitfalls
700+
#### Pitfalls
669701

670702
The following describes some specific pieces of configuration that deserve particularly careful attention,
671703
adapted from https://gist.github.com/mtougeron/1e5d7a30df396cd4728a26b2555e0ef0#file-capz-md.
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
asocontainerservicev1 "github.com/Azure/azure-service-operator/v2/api/containerservice/v1api20231001"
24+
corev1 "k8s.io/api/core/v1"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/runtime"
27+
"k8s.io/utils/ptr"
28+
infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1alpha1"
29+
"sigs.k8s.io/cluster-api-provider-azure/util/tele"
30+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
31+
expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
32+
ctrl "sigs.k8s.io/controller-runtime"
33+
"sigs.k8s.io/controller-runtime/pkg/client"
34+
"sigs.k8s.io/controller-runtime/pkg/event"
35+
"sigs.k8s.io/controller-runtime/pkg/predicate"
36+
)
37+
38+
// AgentPoolAdoptReconciler adopts ASO ManagedClustersAgentPool resources into a CAPI Cluster.
39+
type AgentPoolAdoptReconciler struct {
40+
client.Client
41+
}
42+
43+
// SetupWithManager sets up the controller with the Manager.
44+
func (r *AgentPoolAdoptReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
45+
_, err := ctrl.NewControllerManagedBy(mgr).
46+
For(&asocontainerservicev1.ManagedClustersAgentPool{}).
47+
WithEventFilter(predicate.Funcs{
48+
UpdateFunc: func(ev event.UpdateEvent) bool {
49+
return ev.ObjectOld.GetAnnotations()[adoptAnnotation] != ev.ObjectNew.GetAnnotations()[adoptAnnotation]
50+
},
51+
DeleteFunc: func(_ event.DeleteEvent) bool { return false },
52+
}).
53+
Build(r)
54+
if err != nil {
55+
return err
56+
}
57+
58+
return nil
59+
}
60+
61+
// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinepools,verbs=create
62+
63+
// Reconcile reconciles an AzureASOManagedCluster.
64+
func (r *AgentPoolAdoptReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, resultErr error) {
65+
ctx, log, done := tele.StartSpanWithLogger(ctx,
66+
"controllers.AgentPoolAdoptReconciler.Reconcile",
67+
tele.KVP("namespace", req.Namespace),
68+
tele.KVP("name", req.Name),
69+
tele.KVP("kind", "ManagedCluster"),
70+
)
71+
defer done()
72+
73+
agentPool := &asocontainerservicev1.ManagedClustersAgentPool{}
74+
err := r.Get(ctx, req.NamespacedName, agentPool)
75+
if err != nil {
76+
return ctrl.Result{}, client.IgnoreNotFound(err)
77+
}
78+
79+
if agentPool.GetAnnotations()[adoptAnnotation] != "true" {
80+
return ctrl.Result{}, nil
81+
}
82+
83+
for _, owner := range agentPool.GetOwnerReferences() {
84+
if owner.APIVersion == infrav1exp.GroupVersion.Identifier() &&
85+
owner.Kind == infrav1exp.AzureASOManagedMachinePoolKind {
86+
return ctrl.Result{}, nil
87+
}
88+
}
89+
90+
log.Info("adopting")
91+
92+
namespace := agentPool.Namespace
93+
94+
// filter down to what will be persisted in the AzureASOManagedMachinePool
95+
agentPool = &asocontainerservicev1.ManagedClustersAgentPool{
96+
TypeMeta: metav1.TypeMeta{
97+
APIVersion: asocontainerservicev1.GroupVersion.Identifier(),
98+
Kind: "ManagedClustersAgentPool",
99+
},
100+
ObjectMeta: metav1.ObjectMeta{
101+
Name: agentPool.Name,
102+
},
103+
Spec: agentPool.Spec,
104+
}
105+
106+
var replicas *int32
107+
if agentPool.Spec.Count != nil {
108+
replicas = ptr.To(int32(*agentPool.Spec.Count))
109+
agentPool.Spec.Count = nil
110+
}
111+
112+
managedCluster := &asocontainerservicev1.ManagedCluster{}
113+
if agentPool.Owner() == nil {
114+
return ctrl.Result{}, fmt.Errorf("agent pool %s/%s has no owner", namespace, agentPool.Name)
115+
}
116+
managedClusterKey := client.ObjectKey{
117+
Namespace: namespace,
118+
Name: agentPool.Owner().Name,
119+
}
120+
err = r.Get(ctx, managedClusterKey, managedCluster)
121+
if err != nil {
122+
return ctrl.Result{}, fmt.Errorf("failed to get ManagedCluster %s: %w", managedClusterKey, err)
123+
}
124+
var managedControlPlaneOwner *metav1.OwnerReference
125+
for _, owner := range managedCluster.GetOwnerReferences() {
126+
if owner.APIVersion == infrav1exp.GroupVersion.Identifier() &&
127+
owner.Kind == infrav1exp.AzureASOManagedControlPlaneKind &&
128+
owner.Name == agentPool.Owner().Name {
129+
managedControlPlaneOwner = ptr.To(owner)
130+
break
131+
}
132+
}
133+
if managedControlPlaneOwner == nil {
134+
return ctrl.Result{}, fmt.Errorf("ManagedCluster %s is not owned by any AzureASOManagedControlPlane", managedClusterKey)
135+
}
136+
asoManagedControlPlane := &infrav1exp.AzureASOManagedControlPlane{}
137+
managedControlPlaneKey := client.ObjectKey{
138+
Namespace: namespace,
139+
Name: managedControlPlaneOwner.Name,
140+
}
141+
err = r.Get(ctx, managedControlPlaneKey, asoManagedControlPlane)
142+
if err != nil {
143+
return ctrl.Result{}, fmt.Errorf("failed to get AzureASOManagedControlPlane %s: %w", managedControlPlaneKey, err)
144+
}
145+
clusterName := asoManagedControlPlane.Labels[clusterv1.ClusterNameLabel]
146+
147+
asoManagedMachinePool := &infrav1exp.AzureASOManagedMachinePool{
148+
ObjectMeta: metav1.ObjectMeta{
149+
Namespace: namespace,
150+
Name: agentPool.Name,
151+
},
152+
Spec: infrav1exp.AzureASOManagedMachinePoolSpec{
153+
AzureASOManagedMachinePoolTemplateResourceSpec: infrav1exp.AzureASOManagedMachinePoolTemplateResourceSpec{
154+
Resources: []runtime.RawExtension{
155+
{Object: agentPool},
156+
},
157+
},
158+
},
159+
}
160+
161+
machinePool := &expv1.MachinePool{
162+
ObjectMeta: metav1.ObjectMeta{
163+
Namespace: namespace,
164+
Name: agentPool.Name,
165+
},
166+
Spec: expv1.MachinePoolSpec{
167+
ClusterName: clusterName,
168+
Replicas: replicas,
169+
Template: clusterv1.MachineTemplateSpec{
170+
Spec: clusterv1.MachineSpec{
171+
Bootstrap: clusterv1.Bootstrap{
172+
DataSecretName: ptr.To(""),
173+
},
174+
ClusterName: clusterName,
175+
InfrastructureRef: corev1.ObjectReference{
176+
APIVersion: infrav1exp.GroupVersion.Identifier(),
177+
Kind: infrav1exp.AzureASOManagedMachinePoolKind,
178+
Name: asoManagedMachinePool.Name,
179+
},
180+
},
181+
},
182+
},
183+
}
184+
185+
if ptr.Deref(agentPool.Spec.EnableAutoScaling, false) {
186+
machinePool.Annotations = map[string]string{
187+
clusterv1.ReplicasManagedByAnnotation: infrav1exp.ReplicasManagedByAKS,
188+
}
189+
}
190+
191+
err = r.Create(ctx, machinePool)
192+
if client.IgnoreAlreadyExists(err) != nil {
193+
return ctrl.Result{}, err
194+
}
195+
196+
err = r.Create(ctx, asoManagedMachinePool)
197+
if client.IgnoreAlreadyExists(err) != nil {
198+
return ctrl.Result{}, err
199+
}
200+
201+
return ctrl.Result{}, nil
202+
}

0 commit comments

Comments
 (0)