Skip to content

Commit a982815

Browse files
nojnhuhmtougeron
andcommitted
add AKS adoption e2e test and docs
Co-authored-by: Mike Tougeron <[email protected]>
1 parent f71c5ed commit a982815

File tree

3 files changed

+220
-2
lines changed

3 files changed

+220
-2
lines changed

docs/book/src/topics/managedcluster.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,3 +626,57 @@ Some notes about how this works under the hood:
626626
- CAPZ will fetch the kubeconfig for the AKS cluster and store it in a secret named `${CLUSTER_NAME}-kubeconfig` in the management cluster. That secret is then used for discovery by the `KubeadmConfig` resource.
627627
- You can customize the `MachinePool`, `AzureMachinePool`, and `KubeadmConfig` resources to your liking. The example above is just a starting point. Note that the key configurations to keep are in the `KubeadmConfig` resource, namely the `files`, `joinConfiguration`, and `preKubeadmCommands` sections.
628628
- The `KubeadmConfig` resource will be used to generate a `kubeadm join` command that will be executed on each node in the VMSS. It uses the cluster kubeconfig for discovery. The `kubeadm init phase upload-config all` is run as a preKubeadmCommand to ensure that the kubeadm and kubelet configurations are uploaded to a ConfigMap. This step would normally be done by the `kubeadm init` command, but since we're not running `kubeadm init` we need to do it manually.
629+
630+
## Adopting Existing AKS Clusters
631+
632+
<aside class="note">
633+
634+
<h1> Warning </h1>
635+
636+
Note: This is a newly-supported feature in CAPZ that is less battle-tested than most other features. Potential
637+
bugs or misuse can result in misconfigured or deleted Azure resources. Use with caution.
638+
639+
</aside>
640+
641+
CAPZ can adopt some AKS clusters created by other means under its management. This works by crafting CAPI and
642+
CAPZ manifests which describe the existing cluster and creating those resources on the CAPI management
643+
cluster. This approach is limited to clusters which can be described by the CAPZ API, which includes the
644+
following constraints:
645+
646+
- the cluster operates within a single Virtual Network and Subnet
647+
- the cluster's Virtual Network exists outside of the AKS-managed `MC_*` resource group
648+
- the cluster's Virtual Network and Subnet are not shared with any other resources outside the context of this cluster
649+
650+
To ensure CAPZ does not introduce any unwarranted changes while adopting an existing cluster, carefully review
651+
the [entire AzureManagedControlPlane spec](../reference/v1beta1-api#infrastructure.cluster.x-k8s.io/v1beta1.AzureManagedControlPlaneSpec)
652+
and specify _every_ field in the CAPZ resource. CAPZ's webhooks apply defaults to many fields which may not
653+
match the existing cluster.
654+
655+
Specific AKS features not represented in the CAPZ API, like those from a newer AKS API version than CAPZ uses,
656+
do not need to be specified in the CAPZ resources to remain configured the way they are. CAPZ will still not
657+
be able to manage that configuration, but it will not modify any settings beyond those for which it has
658+
knowledge.
659+
660+
By default, CAPZ will not make any changes to or delete any pre-existing Resource Group, Virtual Network, or
661+
Subnet resources. To opt-in to CAPZ management for those clusters, tag those resources with the following
662+
before creating the CAPZ resources: `sigs.k8s.io_cluster-api-provider-azure_cluster_<CAPI Cluster name>: owned`.
663+
Managed Cluster and Agent Pool resources do not need this tag in order to be adopted.
664+
665+
After applying the CAPI and CAPZ resources for the cluster, other means of managing the cluster should be
666+
disabled to avoid ongoing conflicts with CAPZ's reconciliation process.
667+
668+
### Pitfalls
669+
670+
The following describes some specific pieces of configuration that deserve particularly careful attention,
671+
adapted from https://gist.github.com/mtougeron/1e5d7a30df396cd4728a26b2555e0ef0#file-capz-md.
672+
673+
- Make sure `AzureManagedControlPlane.metadata.name` matches the AKS cluster name
674+
- Set the `AzureManagedControlPlane.spec.virtualNetwork` fields to match your existing VNET
675+
- Make sure the `AzureManagedControlPlane.spec.sshPublicKey` matches what was set on the AKS cluster. (including any potential newlines included in the base64 encoding)
676+
- NOTE: This is a required field in CAPZ, if you don't know what public key was used, you can _change_ or _set_ it via the Azure CLI however before attempting to import the cluster.
677+
- Make sure the `Cluster.spec.clusterNetwork` settings match properly to what you are using in AKS
678+
- Make sure the `AzureManagedControlPlane.spec.dnsServiceIP` matches what is set in AKS
679+
- Set the tag `sigs.k8s.io_cluster-api-provider-azure_cluster_<clusterName>` = `owned` on the AKS cluster
680+
- Set the tag `sigs.k8s.io_cluster-api-provider-azure_role` = `common` on the AKS cluster
681+
682+
NOTE: Several fields, like `networkPlugin`, if not set on the AKS cluster at creation time, will mean that CAPZ will not be able to set that field. AKS doesn't allow such fields to be changed if not set at creation. However, if it was set at creation time, CAPZ will be able to successfully change/manage the field.

test/e2e/aks_adopt.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//go:build e2e
2+
// +build e2e
3+
4+
/*
5+
Copyright 2024 The Kubernetes Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package e2e
21+
22+
import (
23+
"context"
24+
25+
. "github.com/onsi/gomega"
26+
apierrors "k8s.io/apimachinery/pkg/api/errors"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
29+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
30+
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
31+
expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
32+
"sigs.k8s.io/cluster-api/test/framework/clusterctl"
33+
"sigs.k8s.io/controller-runtime/pkg/client"
34+
)
35+
36+
type AKSAdoptSpecInput struct {
37+
ApplyInput clusterctl.ApplyClusterTemplateAndWaitInput
38+
ApplyResult *clusterctl.ApplyClusterTemplateAndWaitResult
39+
Cluster *clusterv1.Cluster
40+
MachinePools []*expv1.MachinePool
41+
}
42+
43+
// AKSAdoptSpec tests adopting an existing AKS cluster into management by CAPZ. It first relies on a CAPZ AKS
44+
// cluster having already been created. Then, it will orphan that cluster such that the CAPI and CAPZ
45+
// resources are deleted but the Azure resources remain. Finally, it applies the cluster template again and
46+
// waits for the cluster to become ready.
47+
func AKSAdoptSpec(ctx context.Context, inputGetter func() AKSAdoptSpecInput) {
48+
input := inputGetter()
49+
50+
mgmtClient := bootstrapClusterProxy.GetClient()
51+
Expect(mgmtClient).NotTo(BeNil())
52+
53+
updateResource := []any{"30s", "5s"}
54+
55+
waitForNoBlockMove := func(obj client.Object) {
56+
waitForBlockMoveGone := []any{"30s", "5s"}
57+
Eventually(func(g Gomega) {
58+
g.Expect(mgmtClient.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed())
59+
g.Expect(obj.GetAnnotations()).NotTo(HaveKey(clusterctlv1.BlockMoveAnnotation))
60+
}, waitForBlockMoveGone...).Should(Succeed())
61+
}
62+
63+
removeFinalizers := func(obj client.Object) {
64+
Eventually(func(g Gomega) {
65+
g.Expect(mgmtClient.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed())
66+
obj.SetFinalizers([]string{})
67+
g.Expect(mgmtClient.Update(ctx, obj)).To(Succeed())
68+
}, updateResource...).Should(Succeed())
69+
}
70+
71+
waitForImmediateDelete := []any{"30s", "5s"}
72+
beginDelete := func(obj client.Object) {
73+
Eventually(func(g Gomega) {
74+
err := mgmtClient.Delete(ctx, obj)
75+
g.Expect(err).NotTo(HaveOccurred())
76+
}, updateResource...).Should(Succeed())
77+
}
78+
shouldNotExist := func(obj client.Object) {
79+
waitForGone := []any{"30s", "5s"}
80+
Eventually(func(g Gomega) {
81+
err := mgmtClient.Get(ctx, client.ObjectKeyFromObject(obj), obj)
82+
g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
83+
}, waitForGone...).Should(Succeed())
84+
}
85+
deleteAndWait := func(obj client.Object) {
86+
Eventually(func(g Gomega) {
87+
err := mgmtClient.Delete(ctx, obj)
88+
g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
89+
}, waitForImmediateDelete...).Should(Succeed())
90+
}
91+
92+
cluster := input.Cluster
93+
Eventually(func(g Gomega) {
94+
g.Expect(mgmtClient.Get(ctx, client.ObjectKeyFromObject(cluster), cluster)).To(Succeed())
95+
cluster.Spec.Paused = true
96+
g.Expect(mgmtClient.Update(ctx, cluster)).To(Succeed())
97+
}, updateResource...).Should(Succeed())
98+
99+
// wait for the pause to take effect before deleting anything
100+
amcp := &infrav1.AzureManagedControlPlane{
101+
ObjectMeta: metav1.ObjectMeta{
102+
Namespace: cluster.Spec.ControlPlaneRef.Namespace,
103+
Name: cluster.Spec.ControlPlaneRef.Name,
104+
},
105+
}
106+
waitForNoBlockMove(amcp)
107+
for _, mp := range input.MachinePools {
108+
ammp := &infrav1.AzureManagedMachinePool{
109+
ObjectMeta: metav1.ObjectMeta{
110+
Namespace: mp.Spec.Template.Spec.InfrastructureRef.Namespace,
111+
Name: mp.Spec.Template.Spec.InfrastructureRef.Name,
112+
},
113+
}
114+
waitForNoBlockMove(ammp)
115+
}
116+
117+
beginDelete(cluster)
118+
119+
for _, mp := range input.MachinePools {
120+
beginDelete(mp)
121+
122+
ammp := &infrav1.AzureManagedMachinePool{
123+
ObjectMeta: metav1.ObjectMeta{
124+
Namespace: mp.Spec.Template.Spec.InfrastructureRef.Namespace,
125+
Name: mp.Spec.Template.Spec.InfrastructureRef.Name,
126+
},
127+
}
128+
removeFinalizers(ammp)
129+
deleteAndWait(ammp)
130+
131+
removeFinalizers(mp)
132+
shouldNotExist(mp)
133+
}
134+
135+
removeFinalizers(amcp)
136+
deleteAndWait(amcp)
137+
// AzureManagedCluster never gets a finalizer
138+
deleteAndWait(&infrav1.AzureManagedCluster{
139+
ObjectMeta: metav1.ObjectMeta{
140+
Namespace: cluster.Spec.InfrastructureRef.Namespace,
141+
Name: cluster.Spec.InfrastructureRef.Name,
142+
},
143+
})
144+
145+
removeFinalizers(cluster)
146+
shouldNotExist(cluster)
147+
148+
clusterctl.ApplyClusterTemplateAndWait(ctx, input.ApplyInput, input.ApplyResult)
149+
}

test/e2e/azure_test.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ var _ = Describe("Workload cluster creation", func() {
699699
Byf("Upgrading to k8s version %s", kubernetesVersion)
700700
Expect(err).NotTo(HaveOccurred())
701701

702-
clusterctl.ApplyClusterTemplateAndWait(ctx, createApplyClusterTemplateInput(
702+
clusterTemplate := createApplyClusterTemplateInput(
703703
specName,
704704
withFlavor("aks"),
705705
withAzureCNIv1Manifest(e2eConfig.GetVariable(AzureCNIv1Manifest)),
@@ -714,7 +714,22 @@ var _ = Describe("Workload cluster creation", func() {
714714
WaitForControlPlaneInitialized: WaitForAKSControlPlaneInitialized,
715715
WaitForControlPlaneMachinesReady: WaitForAKSControlPlaneReady,
716716
}),
717-
), result)
717+
)
718+
719+
clusterctl.ApplyClusterTemplateAndWait(ctx, clusterTemplate, result)
720+
721+
// This test should be first to make sure that the template re-applied here matches the current
722+
// state of the cluster exactly.
723+
By("orphaning and adopting the cluster", func() {
724+
AKSAdoptSpec(ctx, func() AKSAdoptSpecInput {
725+
return AKSAdoptSpecInput{
726+
ApplyInput: clusterTemplate,
727+
ApplyResult: result,
728+
Cluster: result.Cluster,
729+
MachinePools: result.MachinePools,
730+
}
731+
})
732+
})
718733

719734
By("adding an AKS marketplace extension", func() {
720735
AKSMarketplaceExtensionSpec(ctx, func() AKSMarketplaceExtensionSpecInput {

0 commit comments

Comments
 (0)