Skip to content

Commit 82a0292

Browse files
committed
refactor: designate VM instance-specific acceptance tests
1 parent f8d89d7 commit 82a0292

File tree

6 files changed

+2662
-2441
lines changed

6 files changed

+2662
-2441
lines changed
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
/*
2+
Portions Copyright (c) Microsoft Corporation.
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 cloudprovider
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/awslabs/operatorpkg/object"
23+
"github.com/blang/semver/v4"
24+
. "github.com/onsi/ginkgo/v2"
25+
. "github.com/onsi/gomega"
26+
"github.com/samber/lo"
27+
v1 "k8s.io/api/core/v1"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"sigs.k8s.io/controller-runtime/pkg/client"
30+
karpv1 "sigs.k8s.io/karpenter/pkg/apis/v1"
31+
"sigs.k8s.io/karpenter/pkg/controllers/provisioning"
32+
"sigs.k8s.io/karpenter/pkg/controllers/state"
33+
coreoptions "sigs.k8s.io/karpenter/pkg/operator/options"
34+
coretest "sigs.k8s.io/karpenter/pkg/test"
35+
. "sigs.k8s.io/karpenter/pkg/test/expectations"
36+
37+
"github.com/Azure/karpenter-provider-azure/pkg/apis/v1beta1"
38+
"github.com/Azure/karpenter-provider-azure/pkg/consts"
39+
"github.com/Azure/karpenter-provider-azure/pkg/operator/options"
40+
"github.com/Azure/karpenter-provider-azure/pkg/test"
41+
"github.com/Azure/karpenter-provider-azure/pkg/utils"
42+
)
43+
44+
var _ = Describe("CloudProvider", func() {
45+
// Attention: tests under "ProvisionMode = AKSScriptless" are not applicable to ProvisionMode = AKSMachineAPI option.
46+
// Due to different assumptions, not all tests can be shared. Add tests for AKS machine instances in a different Context/file.
47+
// If ProvisionMode = AKSScriptless is no longer supported, their code/tests will be replaced with ProvisionMode = AKSMachineAPI.
48+
Context("ProvisionMode = AKSScriptless", func() {
49+
BeforeEach(func() {
50+
testOptions = test.Options(test.OptionsFields{
51+
ProvisionMode: lo.ToPtr(consts.ProvisionModeAKSScriptless),
52+
})
53+
ctx = coreoptions.ToContext(ctx, coretest.Options())
54+
ctx = options.ToContext(ctx, testOptions)
55+
56+
azureEnv = test.NewEnvironment(ctx, env)
57+
test.ApplyDefaultStatus(nodeClass, env, testOptions.UseSIG)
58+
cloudProvider = New(azureEnv.InstanceTypesProvider, azureEnv.VMInstanceProvider, recorder, env.Client, azureEnv.ImageProvider)
59+
60+
cluster = state.NewCluster(fakeClock, env.Client, cloudProvider)
61+
coreProvisioner = provisioning.NewProvisioner(env.Client, recorder, cloudProvider, cluster, fakeClock)
62+
})
63+
64+
AfterEach(func() {
65+
cluster.Reset()
66+
azureEnv.Reset()
67+
})
68+
69+
Context("Drift", func() {
70+
var driftNodeClaim *karpv1.NodeClaim
71+
var pod *v1.Pod
72+
var node *v1.Node
73+
74+
BeforeEach(func() {
75+
// Set up VM provisioning mode environment for drift testing
76+
testOptions = test.Options()
77+
ctx = coreoptions.ToContext(ctx, coretest.Options())
78+
ctx = options.ToContext(ctx, testOptions)
79+
azureEnv = test.NewEnvironment(ctx, env)
80+
test.ApplyDefaultStatus(nodeClass, env, testOptions.UseSIG)
81+
cloudProvider = New(azureEnv.InstanceTypesProvider, azureEnv.VMInstanceProvider, recorder, env.Client, azureEnv.ImageProvider)
82+
cluster = state.NewCluster(fakeClock, env.Client, cloudProvider)
83+
coreProvisioner = provisioning.NewProvisioner(env.Client, recorder, cloudProvider, cluster, fakeClock)
84+
85+
instanceType := "Standard_D2_v2"
86+
ExpectApplied(ctx, env.Client, nodePool, nodeClass)
87+
pod = coretest.UnschedulablePod(coretest.PodOptions{
88+
NodeSelector: map[string]string{v1.LabelInstanceTypeStable: instanceType},
89+
})
90+
ExpectProvisioned(ctx, env.Client, cluster, cloudProvider, coreProvisioner, pod)
91+
node = ExpectScheduled(ctx, env.Client, pod)
92+
// KubeletVersion must be applied to the node to satisfy k8s drift
93+
node.Status.NodeInfo.KubeletVersion = "v" + nodeClass.Status.KubernetesVersion
94+
node.Labels[v1beta1.AKSLabelKubeletIdentityClientID] = "61f71907-753f-4802-a901-47361c3664f2" // random UUID
95+
// Context must have same kubelet client id
96+
ctx = options.ToContext(ctx, test.Options(test.OptionsFields{
97+
KubeletIdentityClientID: lo.ToPtr(node.Labels[v1beta1.AKSLabelKubeletIdentityClientID]),
98+
}))
99+
100+
ExpectApplied(ctx, env.Client, node)
101+
Expect(azureEnv.NetworkInterfacesAPI.NetworkInterfacesCreateOrUpdateBehavior.CalledWithInput.Len()).To(Equal(1))
102+
Expect(azureEnv.VirtualMachinesAPI.VirtualMachineCreateOrUpdateBehavior.CalledWithInput.Len()).To(Equal(1))
103+
input := azureEnv.VirtualMachinesAPI.VirtualMachineCreateOrUpdateBehavior.CalledWithInput.Pop()
104+
rg := input.ResourceGroupName
105+
vmName := input.VMName
106+
// Corresponding NodeClaim
107+
driftNodeClaim = coretest.NodeClaim(karpv1.NodeClaim{
108+
Status: karpv1.NodeClaimStatus{
109+
NodeName: node.Name,
110+
// TODO (charliedmcb): switch back to use MkVMID, and update the test subscription usage to all use the same sub const 12345678-1234-1234-1234-123456789012
111+
// We currently need this work around for the List nodes call to work in Drift, since the VM ID is overridden here (which uses the sub id in the instance provider):
112+
// https://github.com/Azure/karpenter-provider-azure/blob/84e449787ec72268efb0c7af81ec87a6b3ee95fa/pkg/providers/instance/instance.go#L604
113+
// which has the sub const 12345678-1234-1234-1234-123456789012 passed in here:
114+
// https://github.com/Azure/karpenter-provider-azure/blob/84e449787ec72268efb0c7af81ec87a6b3ee95fa/pkg/test/environment.go#L152
115+
ProviderID: utils.VMResourceIDToProviderID(ctx, fmt.Sprintf("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s", rg, vmName)),
116+
},
117+
ObjectMeta: metav1.ObjectMeta{
118+
Labels: map[string]string{
119+
karpv1.NodePoolLabelKey: nodePool.Name,
120+
v1.LabelInstanceTypeStable: instanceType,
121+
},
122+
},
123+
Spec: karpv1.NodeClaimSpec{
124+
NodeClassRef: &karpv1.NodeClassReference{
125+
Group: object.GVK(nodeClass).Group,
126+
Kind: object.GVK(nodeClass).Kind,
127+
Name: nodeClass.Name,
128+
},
129+
},
130+
})
131+
})
132+
133+
It("should not fail if nodeClass does not exist", func() {
134+
ExpectDeleted(ctx, env.Client, nodeClass)
135+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
136+
Expect(err).ToNot(HaveOccurred())
137+
Expect(drifted).To(BeEmpty())
138+
})
139+
140+
It("should not fail if nodePool does not exist", func() {
141+
ExpectDeleted(ctx, env.Client, nodePool)
142+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
143+
Expect(err).ToNot(HaveOccurred())
144+
Expect(drifted).To(BeEmpty())
145+
})
146+
147+
It("should not return drifted if the NodeClaim is valid", func() {
148+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
149+
Expect(err).ToNot(HaveOccurred())
150+
Expect(drifted).To(BeEmpty())
151+
})
152+
153+
It("should error drift if NodeClaim doesn't have provider id", func() {
154+
driftNodeClaim.Status = karpv1.NodeClaimStatus{}
155+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
156+
Expect(err).To(HaveOccurred())
157+
Expect(drifted).To(BeEmpty())
158+
})
159+
160+
Context("Node Image Drift", func() {
161+
It("should succeed with no drift when nothing changes", func() {
162+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
163+
Expect(err).ToNot(HaveOccurred())
164+
Expect(drifted).To(Equal(NoDrift))
165+
})
166+
167+
It("should succeed with no drift when ConditionTypeImagesReady is not true", func() {
168+
nodeClass = ExpectExists(ctx, env.Client, nodeClass)
169+
nodeClass.StatusConditions().SetFalse(v1beta1.ConditionTypeImagesReady, "ImagesNoLongerReady", "test when images aren't ready")
170+
ExpectApplied(ctx, env.Client, nodeClass)
171+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
172+
Expect(err).ToNot(HaveOccurred())
173+
Expect(drifted).To(Equal(NoDrift))
174+
})
175+
176+
// Note: this case shouldn't be able to happen in practice since if Images is empty ConditionTypeImagesReady should be false.
177+
It("should error when Images are empty", func() {
178+
nodeClass = ExpectExists(ctx, env.Client, nodeClass)
179+
nodeClass.Status.Images = []v1beta1.NodeImage{}
180+
ExpectApplied(ctx, env.Client, nodeClass)
181+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
182+
Expect(err).To(HaveOccurred())
183+
Expect(drifted).To(Equal(NoDrift))
184+
})
185+
186+
It("should trigger drift when the image gallery changes to SIG", func() {
187+
test.ApplySIGImages(nodeClass)
188+
ExpectApplied(ctx, env.Client, nodeClass)
189+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
190+
Expect(err).ToNot(HaveOccurred())
191+
Expect(drifted).To(Equal(ImageDrift))
192+
})
193+
194+
It("should trigger drift when the image version changes", func() {
195+
test.ApplyCIGImagesWithVersion(nodeClass, "202503.02.0")
196+
ExpectApplied(ctx, env.Client, nodeClass)
197+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
198+
Expect(err).ToNot(HaveOccurred())
199+
Expect(drifted).To(Equal(ImageDrift))
200+
})
201+
})
202+
203+
Context("Kubernetes Version", func() {
204+
It("should succeed with no drift when nothing changes", func() {
205+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
206+
Expect(err).ToNot(HaveOccurred())
207+
Expect(drifted).To(Equal(NoDrift))
208+
})
209+
210+
It("should succeed with no drift when KubernetesVersionReady is not true", func() {
211+
nodeClass = ExpectExists(ctx, env.Client, nodeClass)
212+
nodeClass.StatusConditions().SetFalse(v1beta1.ConditionTypeKubernetesVersionReady, "K8sVersionNoLongerReady", "test when k8s isn't ready")
213+
ExpectApplied(ctx, env.Client, nodeClass)
214+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
215+
Expect(err).ToNot(HaveOccurred())
216+
Expect(drifted).To(Equal(NoDrift))
217+
})
218+
219+
// TODO (charliedmcb): I'm wondering if we actually want to have these soft-error cases switch to return an error if no-drift condition was found.
220+
It("shouldn't error or be drifted when KubernetesVersion is empty", func() {
221+
nodeClass = ExpectExists(ctx, env.Client, nodeClass)
222+
nodeClass.Status.KubernetesVersion = ""
223+
ExpectApplied(ctx, env.Client, nodeClass)
224+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
225+
Expect(err).ToNot(HaveOccurred())
226+
Expect(drifted).To(Equal(NoDrift))
227+
})
228+
229+
It("shouldn't error or be drifted when NodeName is missing", func() {
230+
driftNodeClaim.Status.NodeName = ""
231+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
232+
Expect(err).ToNot(HaveOccurred())
233+
Expect(drifted).To(Equal(NoDrift))
234+
})
235+
236+
It("shouldn't error or be drifted when node is not found", func() {
237+
driftNodeClaim.Status.NodeName = "NodeWhoDoesNotExist"
238+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
239+
Expect(err).ToNot(HaveOccurred())
240+
Expect(drifted).To(Equal(NoDrift))
241+
})
242+
243+
It("shouldn't error or be drifted when node is deleting", func() {
244+
node = ExpectNodeExists(ctx, env.Client, driftNodeClaim.Status.NodeName)
245+
node.Finalizers = append(node.Finalizers, test.TestingFinalizer)
246+
ExpectApplied(ctx, env.Client, node)
247+
Expect(env.Client.Delete(ctx, node)).ToNot(HaveOccurred())
248+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
249+
Expect(err).ToNot(HaveOccurred())
250+
Expect(drifted).To(Equal(NoDrift))
251+
252+
// cleanup
253+
node = ExpectNodeExists(ctx, env.Client, driftNodeClaim.Status.NodeName)
254+
deepCopy := node.DeepCopy()
255+
node.Finalizers = lo.Reject(node.Finalizers, func(finalizer string, _ int) bool {
256+
return finalizer == test.TestingFinalizer
257+
})
258+
Expect(env.Client.Patch(ctx, node, client.StrategicMergeFrom(deepCopy))).NotTo(HaveOccurred())
259+
ExpectDeleted(ctx, env.Client, node)
260+
})
261+
262+
It("should succeed with drift true when KubernetesVersion is new", func() {
263+
nodeClass = ExpectExists(ctx, env.Client, nodeClass)
264+
265+
semverCurrentK8sVersion := lo.Must(semver.ParseTolerant(nodeClass.Status.KubernetesVersion))
266+
semverCurrentK8sVersion.Minor = semverCurrentK8sVersion.Minor + 1
267+
nodeClass.Status.KubernetesVersion = semverCurrentK8sVersion.String()
268+
269+
ExpectApplied(ctx, env.Client, nodeClass)
270+
271+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
272+
Expect(err).ToNot(HaveOccurred())
273+
Expect(drifted).To(Equal(K8sVersionDrift))
274+
})
275+
})
276+
277+
Context("Kubelet Client ID", func() {
278+
It("should NOT trigger drift if node doesn't have kubelet client ID label", func() {
279+
node.Labels[v1beta1.AKSLabelKubeletIdentityClientID] = "" // Not set
280+
281+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
282+
Expect(err).ToNot(HaveOccurred())
283+
Expect(drifted).To(BeEmpty())
284+
})
285+
286+
It("should trigger drift if node kubelet client ID doesn't match options", func() {
287+
ctx = options.ToContext(ctx, test.Options(test.OptionsFields{
288+
KubeletIdentityClientID: lo.ToPtr("3824ff7a-93b6-40af-b861-2eb621ba437a"), // a different random UUID
289+
}))
290+
291+
drifted, err := cloudProvider.IsDrifted(ctx, driftNodeClaim)
292+
Expect(err).ToNot(HaveOccurred())
293+
Expect(drifted).To(Equal(KubeletIdentityDrift))
294+
})
295+
})
296+
297+
})
298+
})
299+
})

0 commit comments

Comments
 (0)