Skip to content

Commit 5e1aef9

Browse files
author
Amarthya Valija
committed
Add NodePool STS permissions test
1 parent d3ba311 commit 5e1aef9

File tree

1 file changed

+311
-0
lines changed

1 file changed

+311
-0
lines changed

pkg/e2e/verify/nodepool.go

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
package verify
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"time"
8+
9+
"github.com/onsi/ginkgo/v2"
10+
. "github.com/onsi/gomega"
11+
12+
viper "github.com/openshift/osde2e/pkg/common/concurrentviper"
13+
"github.com/openshift/osde2e/pkg/common/config"
14+
"github.com/openshift/osde2e/pkg/common/expect"
15+
"github.com/openshift/osde2e/pkg/common/helper"
16+
"github.com/openshift/osde2e/pkg/common/label"
17+
corev1 "k8s.io/api/core/v1"
18+
apierrors "k8s.io/apimachinery/pkg/api/errors"
19+
"k8s.io/apimachinery/pkg/api/resource"
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
22+
"k8s.io/apimachinery/pkg/runtime/schema"
23+
"sigs.k8s.io/e2e-framework/klient/k8s/resources"
24+
"sigs.k8s.io/e2e-framework/klient/wait"
25+
"sigs.k8s.io/e2e-framework/klient/wait/conditions"
26+
)
27+
28+
var _ = ginkgo.Describe("[Suite: e2e] NodePool STS Permissions", ginkgo.Ordered, label.HyperShift, label.E2E, func() {
29+
var h *helper.H
30+
var client *resources.Resources
31+
var clusterNamespace string
32+
var testNodePoolName string
33+
var initialNodeCount int
34+
35+
nodePoolGVR := schema.GroupVersionResource{
36+
Group: "hypershift.openshift.io",
37+
Version: "v1beta1",
38+
Resource: "nodepools",
39+
}
40+
41+
ginkgo.BeforeAll(func() {
42+
if !viper.GetBool(config.Hypershift) {
43+
ginkgo.Skip("NodePool tests are only supported on HyperShift clusters")
44+
}
45+
46+
h = helper.New()
47+
client = h.AsUser("")
48+
49+
var nodeList corev1.NodeList
50+
expect.NoError(client.List(context.Background(), &nodeList))
51+
52+
if len(nodeList.Items) == 0 {
53+
ginkgo.Skip("No nodes found - cannot run NodePool tests")
54+
}
55+
56+
initialNodeCount = len(nodeList.Items)
57+
58+
for _, node := range nodeList.Items {
59+
if label, exists := node.Labels["hypershift.openshift.io/nodePool"]; exists {
60+
parts := strings.Split(label, "-workers-")
61+
if len(parts) >= 1 {
62+
clusterNamespace = parts[0]
63+
break
64+
}
65+
}
66+
}
67+
68+
if clusterNamespace == "" {
69+
ginkgo.Skip("Could not determine cluster namespace from node labels")
70+
}
71+
72+
testNodePoolName = fmt.Sprintf("test-%d", time.Now().Unix()%100000)
73+
})
74+
75+
ginkgo.AfterAll(func() {
76+
ctx := context.Background()
77+
if testNodePoolName != "" {
78+
ginkgo.By("Cleaning up test NodePool")
79+
80+
// Delete the NodePool
81+
err := h.Dynamic().Resource(nodePoolGVR).Namespace(clusterNamespace).
82+
Delete(ctx, testNodePoolName, metav1.DeleteOptions{})
83+
if err != nil && !apierrors.IsNotFound(err) {
84+
ginkgo.GinkgoLogr.Error(err, "Failed to cleanup test NodePool", "name", testNodePoolName)
85+
}
86+
87+
// Wait for NodePool deletion to complete
88+
ginkgo.By("Waiting for NodePool deletion to complete")
89+
wait.For(func(ctx context.Context) (bool, error) {
90+
_, err := h.Dynamic().Resource(nodePoolGVR).Namespace(clusterNamespace).
91+
Get(ctx, testNodePoolName, metav1.GetOptions{})
92+
return apierrors.IsNotFound(err), nil
93+
}, wait.WithTimeout(5*time.Minute), wait.WithInterval(30*time.Second))
94+
95+
// Wait for nodes to be removed
96+
ginkgo.By("Waiting for test nodes to be terminated")
97+
wait.For(func(ctx context.Context) (bool, error) {
98+
nodeList := &corev1.NodeList{}
99+
err := client.List(ctx, nodeList)
100+
if err != nil {
101+
return false, err
102+
}
103+
104+
// Check if any nodes still have the test NodePool label
105+
for _, node := range nodeList.Items {
106+
if label, exists := node.Labels["hypershift.openshift.io/nodePool"]; exists {
107+
if strings.Contains(label, testNodePoolName) {
108+
return false, nil // Still have test nodes
109+
}
110+
}
111+
}
112+
return true, nil // No test nodes remaining
113+
}, wait.WithTimeout(10*time.Minute), wait.WithInterval(30*time.Second))
114+
}
115+
})
116+
117+
ginkgo.It("should successfully create NodePool", func(ctx context.Context) {
118+
ginkgo.By("Getting existing NodePool configuration")
119+
120+
existingNodePools, err := h.Dynamic().Resource(nodePoolGVR).Namespace(clusterNamespace).List(ctx, metav1.ListOptions{})
121+
expect.NoError(err, "Failed to list existing NodePools")
122+
Expect(len(existingNodePools.Items)).To(BeNumerically(">", 0), "No existing NodePools found to reference")
123+
124+
ginkgo.By("Creating test NodePool to validate STS permissions")
125+
126+
refNodePool := existingNodePools.Items[0]
127+
var subnet string
128+
if spec, found, err := unstructured.NestedMap(refNodePool.Object, "spec"); found && err == nil {
129+
if platform, found, err := unstructured.NestedMap(spec, "platform"); found && err == nil {
130+
if aws, found, err := unstructured.NestedMap(platform, "aws"); found && err == nil {
131+
if s, found, err := unstructured.NestedString(aws, "subnet"); found && err == nil {
132+
subnet = s
133+
}
134+
}
135+
}
136+
}
137+
138+
nodePoolSpec := map[string]interface{}{
139+
"apiVersion": "hypershift.openshift.io/v1beta1",
140+
"kind": "NodePool",
141+
"metadata": map[string]interface{}{
142+
"name": testNodePoolName,
143+
"namespace": clusterNamespace,
144+
},
145+
"spec": map[string]interface{}{
146+
"clusterName": clusterNamespace,
147+
"replicas": 1,
148+
"management": map[string]interface{}{
149+
"autoRepair": true,
150+
"upgradeType": "Replace",
151+
},
152+
"platform": map[string]interface{}{
153+
"aws": map[string]interface{}{
154+
"instanceType": "m5.large",
155+
},
156+
},
157+
},
158+
}
159+
160+
if subnet != "" {
161+
spec := nodePoolSpec["spec"].(map[string]interface{})
162+
platform := spec["platform"].(map[string]interface{})
163+
aws := platform["aws"].(map[string]interface{})
164+
aws["subnet"] = subnet
165+
}
166+
167+
nodePoolObj := &unstructured.Unstructured{Object: nodePoolSpec}
168+
_, err = h.Dynamic().Resource(nodePoolGVR).Namespace(clusterNamespace).Create(ctx, nodePoolObj, metav1.CreateOptions{})
169+
expect.NoError(err, "NodePool creation failed - STS permissions (ec2:RunInstances, ec2:CreateTags) missing")
170+
})
171+
172+
ginkgo.It("should provision nodes with correct labels", func(ctx context.Context) {
173+
ginkgo.By("Waiting for new nodes to be provisioned")
174+
175+
var newNodes []corev1.Node
176+
err := wait.For(func(ctx context.Context) (bool, error) {
177+
var nodeList corev1.NodeList
178+
err := client.List(ctx, &nodeList)
179+
if err != nil {
180+
return false, err
181+
}
182+
183+
newNodes = nil
184+
if len(nodeList.Items) > initialNodeCount {
185+
for _, node := range nodeList.Items {
186+
if label, exists := node.Labels["hypershift.openshift.io/nodePool"]; exists {
187+
if strings.Contains(label, testNodePoolName) && isNodeReady(node) {
188+
newNodes = append(newNodes, node)
189+
}
190+
}
191+
}
192+
}
193+
return len(newNodes) > 0, nil
194+
}, wait.WithTimeout(20*time.Minute), wait.WithInterval(30*time.Second))
195+
196+
expect.NoError(err, "NodePool failed to provision nodes - STS permissions (ec2:RunInstances) may be missing")
197+
Expect(len(newNodes)).To(BeNumerically(">", 0), "No new nodes found")
198+
199+
ginkgo.By("Validating node has proper AWS integration")
200+
201+
for _, node := range newNodes {
202+
nodePoolLabel, exists := node.Labels["hypershift.openshift.io/nodePool"]
203+
Expect(exists).To(BeTrue(), "Node %s missing NodePool label", node.Name)
204+
Expect(nodePoolLabel).To(ContainSubstring(testNodePoolName),
205+
"Node %s has incorrect NodePool label", node.Name)
206+
207+
Expect(node.Spec.ProviderID).To(HavePrefix("aws://"),
208+
"Node %s should have AWS provider ID - ec2:DescribeInstances permission may be missing", node.Name)
209+
210+
hasInternalIP := false
211+
for _, addr := range node.Status.Addresses {
212+
if addr.Type == corev1.NodeInternalIP {
213+
hasInternalIP = true
214+
Expect(addr.Address).To(MatchRegexp(`^10\.`),
215+
"Node %s should have VPC internal IP", node.Name)
216+
break
217+
}
218+
}
219+
Expect(hasInternalIP).To(BeTrue(), "Node %s should have internal IP", node.Name)
220+
}
221+
})
222+
223+
ginkgo.It("should schedule workloads on new NodePool nodes", func(ctx context.Context) {
224+
ginkgo.By("Creating test workload targeted at NodePool")
225+
226+
pod := &corev1.Pod{
227+
ObjectMeta: metav1.ObjectMeta{
228+
GenerateName: "nodepool-test-",
229+
Namespace: h.CurrentProject(),
230+
},
231+
Spec: corev1.PodSpec{
232+
NodeSelector: map[string]string{
233+
"hypershift.openshift.io/nodePool": fmt.Sprintf("%s-%s", clusterNamespace, testNodePoolName),
234+
},
235+
Containers: []corev1.Container{{
236+
Name: "test",
237+
Image: "registry.access.redhat.com/ubi8/ubi-minimal",
238+
Command: []string{"/bin/sh", "-c", "echo 'NodePool workload test successful' && sleep 5"},
239+
Resources: corev1.ResourceRequirements{
240+
Requests: corev1.ResourceList{
241+
corev1.ResourceCPU: resource.MustParse("100m"),
242+
corev1.ResourceMemory: resource.MustParse("128Mi"),
243+
},
244+
},
245+
}},
246+
RestartPolicy: corev1.RestartPolicyNever,
247+
},
248+
}
249+
250+
expect.NoError(client.Create(ctx, pod), "Failed to create test pod")
251+
252+
ginkgo.By("Waiting for workload to complete successfully")
253+
254+
err := wait.For(conditions.New(client).PodPhaseMatch(pod, corev1.PodSucceeded), wait.WithTimeout(5*time.Minute))
255+
expect.NoError(err, "Workload scheduling failed on NodePool")
256+
257+
expect.NoError(client.Delete(ctx, pod), "Failed to delete test pod")
258+
})
259+
260+
ginkgo.It("should reject duplicate NodePool names", func(ctx context.Context) {
261+
ginkgo.By("Testing duplicate NodePool creation")
262+
263+
duplicateNodePoolSpec := map[string]interface{}{
264+
"apiVersion": "hypershift.openshift.io/v1beta1",
265+
"kind": "NodePool",
266+
"metadata": map[string]interface{}{
267+
"name": testNodePoolName,
268+
"namespace": clusterNamespace,
269+
},
270+
"spec": map[string]interface{}{
271+
"clusterName": clusterNamespace,
272+
"replicas": 1,
273+
"platform": map[string]interface{}{
274+
"aws": map[string]interface{}{
275+
"instanceType": "m5.large",
276+
},
277+
},
278+
},
279+
}
280+
281+
duplicateNodePoolObj := &unstructured.Unstructured{Object: duplicateNodePoolSpec}
282+
_, err := h.Dynamic().Resource(nodePoolGVR).Namespace(clusterNamespace).Create(ctx, duplicateNodePoolObj, metav1.CreateOptions{})
283+
Expect(err).To(HaveOccurred(), "Should fail when creating NodePool with duplicate name")
284+
})
285+
286+
ginkgo.It("should reject operations on non-existent NodePool", func(ctx context.Context) {
287+
ginkgo.By("Testing access to non-existent NodePool")
288+
289+
testNodePool := &unstructured.Unstructured{}
290+
testNodePool.SetGroupVersionKind(schema.GroupVersionKind{
291+
Group: nodePoolGVR.Group,
292+
Version: nodePoolGVR.Version,
293+
Kind: "NodePool",
294+
})
295+
296+
err := h.Dynamic().Resource(nodePoolGVR).Namespace(clusterNamespace).
297+
Get(ctx, "non-existent-nodepool", testNodePool, metav1.GetOptions{})
298+
299+
Expect(err).To(HaveOccurred(), "Getting non-existent NodePool should fail")
300+
Expect(apierrors.IsNotFound(err)).To(BeTrue(), "Should return NotFound error")
301+
})
302+
})
303+
304+
func isNodeReady(node corev1.Node) bool {
305+
for _, condition := range node.Status.Conditions {
306+
if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
307+
return true
308+
}
309+
}
310+
return false
311+
}

0 commit comments

Comments
 (0)