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