1
2
3
4 package e2eutil
5
6 import (
7 "bytes"
8 "context"
9 "fmt"
10 "text/template"
11 "time"
12
13 "github.com/google/go-cmp/cmp"
14 "github.com/google/go-cmp/cmp/cmpopts"
15 "github.com/onsi/ginkgo/v2"
16 "github.com/onsi/gomega"
17 "github.com/onsi/gomega/gstruct"
18 v1 "k8s.io/api/core/v1"
19 apierrors "k8s.io/apimachinery/pkg/api/errors"
20 "k8s.io/apimachinery/pkg/api/meta"
21 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
23 "k8s.io/apimachinery/pkg/types"
24 "k8s.io/apimachinery/pkg/util/yaml"
25 "k8s.io/client-go/rest"
26 "sigs.k8s.io/cli-utils/pkg/apply/event"
27 "sigs.k8s.io/cli-utils/pkg/common"
28 "sigs.k8s.io/cli-utils/pkg/flowcontrol"
29 "sigs.k8s.io/cli-utils/pkg/kstatus/status"
30 "sigs.k8s.io/cli-utils/pkg/object/dependson"
31 "sigs.k8s.io/cli-utils/pkg/object/mutation"
32 "sigs.k8s.io/cli-utils/pkg/testutil"
33 "sigs.k8s.io/cli-utils/test/e2e/customprovider"
34 "sigs.k8s.io/controller-runtime/pkg/client"
35 )
36
37 const TestIDLabel = "test-id"
38
39 func WithReplicas(obj *unstructured.Unstructured, replicas int) *unstructured.Unstructured {
40 err := unstructured.SetNestedField(obj.Object, int64(replicas), "spec", "replicas")
41 gomega.Expect(err).NotTo(gomega.HaveOccurred())
42 return obj
43 }
44
45 func WithNamespace(obj *unstructured.Unstructured, namespace string) *unstructured.Unstructured {
46 obj.SetNamespace(namespace)
47 return obj
48 }
49
50 func PodWithImage(obj *unstructured.Unstructured, containerName, image string) *unstructured.Unstructured {
51 containers, found, err := unstructured.NestedSlice(obj.Object, "spec", "containers")
52 gomega.Expect(err).NotTo(gomega.HaveOccurred())
53 gomega.Expect(found).To(gomega.BeTrue())
54
55 containerFound := false
56 for i := range containers {
57 container := containers[i].(map[string]interface{})
58 name := container["name"].(string)
59 if name != containerName {
60 continue
61 }
62 containerFound = true
63 container["image"] = image
64 }
65 gomega.Expect(containerFound).To(gomega.BeTrue())
66 err = unstructured.SetNestedSlice(obj.Object, containers, "spec", "containers")
67 gomega.Expect(err).NotTo(gomega.HaveOccurred())
68 return obj
69 }
70
71 func WithNodeSelector(obj *unstructured.Unstructured, key, value string) *unstructured.Unstructured {
72 selectors, found, err := unstructured.NestedMap(obj.Object, "spec", "nodeSelector")
73 gomega.Expect(err).NotTo(gomega.HaveOccurred())
74
75 if !found {
76 selectors = make(map[string]interface{})
77 }
78 selectors[key] = value
79 err = unstructured.SetNestedMap(obj.Object, selectors, "spec", "nodeSelector")
80 gomega.Expect(err).NotTo(gomega.HaveOccurred())
81 return obj
82 }
83
84 func WithAnnotation(obj *unstructured.Unstructured, key, value string) *unstructured.Unstructured {
85 annotations := obj.GetAnnotations()
86 if annotations == nil {
87 annotations = make(map[string]string)
88 }
89 annotations[key] = value
90 obj.SetAnnotations(annotations)
91 return obj
92 }
93
94 func WithDependsOn(obj *unstructured.Unstructured, dep string) *unstructured.Unstructured {
95 a := obj.GetAnnotations()
96 if a == nil {
97 a = make(map[string]string, 1)
98 }
99 a[dependson.Annotation] = dep
100 obj.SetAnnotations(a)
101 return obj
102 }
103
104 func DeleteUnstructuredAndWait(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
105 ref := mutation.ResourceReferenceFromUnstructured(obj)
106
107 err := c.Delete(ctx, obj,
108 client.PropagationPolicy(metav1.DeletePropagationForeground))
109 gomega.Expect(err).NotTo(gomega.HaveOccurred(),
110 "expected DELETE to not error (%s): %s", ref, err)
111
112 WaitForDeletion(ctx, c, obj)
113 }
114
115 func WaitForDeletion(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
116 ref := mutation.ResourceReferenceFromUnstructured(obj)
117 resultObj := ref.ToUnstructured()
118
119 timeout := 30 * time.Second
120 retry := 2 * time.Second
121
122 t := time.NewTimer(timeout)
123 s := time.NewTimer(0)
124 defer t.Stop()
125
126 for {
127 select {
128 case <-t.C:
129 ginkgo.Fail("timed out waiting for resource to be fully deleted")
130 return
131 case <-s.C:
132 err := c.Get(ctx, types.NamespacedName{
133 Namespace: obj.GetNamespace(),
134 Name: obj.GetName(),
135 }, resultObj)
136 if err != nil {
137 gomega.Expect(apierrors.ReasonForError(err)).To(gomega.Equal(metav1.StatusReasonNotFound),
138 "expected GET to error with NotFound (%s): %s", ref, err)
139 return
140 }
141 s = time.NewTimer(retry)
142 }
143 }
144 }
145
146 func CreateUnstructuredAndWait(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
147 ref := mutation.ResourceReferenceFromUnstructured(obj)
148
149 err := c.Create(ctx, obj)
150 gomega.Expect(err).NotTo(gomega.HaveOccurred(),
151 "expected CREATE to not error (%s): %s", ref, err)
152
153 WaitForCreation(ctx, c, obj)
154 }
155
156 func WaitForCreation(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
157 ref := mutation.ResourceReferenceFromUnstructured(obj)
158 resultObj := ref.ToUnstructured()
159
160 timeout := 30 * time.Second
161 retry := 2 * time.Second
162
163 t := time.NewTimer(timeout)
164 s := time.NewTimer(0)
165 defer t.Stop()
166
167 for {
168 select {
169 case <-t.C:
170 ginkgo.Fail("timed out waiting for resource to be fully created")
171 return
172 case <-s.C:
173 err := c.Get(ctx, types.NamespacedName{
174 Namespace: obj.GetNamespace(),
175 Name: obj.GetName(),
176 }, resultObj)
177 if err == nil {
178 return
179 }
180 gomega.Expect(apierrors.ReasonForError(err)).To(gomega.Equal(metav1.StatusReasonNotFound),
181 "expected GET to error with NotFound (%s): %s", ref, err)
182
183 s = time.NewTimer(retry)
184 }
185 }
186 }
187
188 func AssertUnstructuredExists(ctx context.Context, c client.Client, obj *unstructured.Unstructured) *unstructured.Unstructured {
189 ref := mutation.ResourceReferenceFromUnstructured(obj)
190 resultObj := ref.ToUnstructured()
191
192 err := c.Get(ctx, types.NamespacedName{
193 Namespace: obj.GetNamespace(),
194 Name: obj.GetName(),
195 }, resultObj)
196 gomega.Expect(err).NotTo(gomega.HaveOccurred(),
197 "expected GET not to error (%s): %s", ref, err)
198 return resultObj
199 }
200
201 func AssertUnstructuredDoesNotExist(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
202 ref := mutation.ResourceReferenceFromUnstructured(obj)
203 resultObj := ref.ToUnstructured()
204
205 err := c.Get(ctx, types.NamespacedName{
206 Namespace: obj.GetNamespace(),
207 Name: obj.GetName(),
208 }, resultObj)
209 gomega.Expect(err).To(gomega.HaveOccurred(),
210 "expected GET to error (%s)", ref)
211 gomega.Expect(apierrors.ReasonForError(err)).To(gomega.Equal(metav1.StatusReasonNotFound),
212 "expected GET to error with NotFound (%s): %s", ref, err)
213 }
214
215 func ApplyUnstructured(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
216 ref := mutation.ResourceReferenceFromUnstructured(obj)
217 resultObj := ref.ToUnstructured()
218
219 err := c.Get(ctx, types.NamespacedName{
220 Namespace: obj.GetNamespace(),
221 Name: obj.GetName(),
222 }, resultObj)
223 gomega.Expect(err).NotTo(gomega.HaveOccurred(),
224 "expected GET not to error (%s)", ref)
225
226 err = c.Patch(ctx, obj, client.MergeFrom(resultObj))
227 gomega.Expect(err).NotTo(gomega.HaveOccurred(),
228 "expected PATCH not to error (%s): %s", ref, err)
229 }
230
231 func AssertUnstructuredAvailable(obj *unstructured.Unstructured) {
232 ref := mutation.ResourceReferenceFromUnstructured(obj)
233 objc, err := status.GetObjectWithConditions(obj.Object)
234 gomega.Expect(err).NotTo(gomega.HaveOccurred())
235 available := false
236 for _, c := range objc.Status.Conditions {
237
238 if c.Type == "Available" && c.Status == "True" {
239 available = true
240 break
241 }
242 }
243 gomega.Expect(available).To(gomega.BeTrue(),
244 "expected Available condition to be True (%s)", ref)
245 }
246
247 func AssertUnstructuredCount(ctx context.Context, c client.Client, obj *unstructured.Unstructured, count int) {
248 var u unstructured.UnstructuredList
249 u.SetGroupVersionKind(obj.GetObjectKind().GroupVersionKind())
250 err := c.List(ctx, &u,
251 client.InNamespace(obj.GetNamespace()),
252 client.MatchingLabels(obj.GetLabels()))
253 if err != nil && count == 0 {
254 expectNotFoundError(err)
255 return
256 }
257 gomega.Expect(err).NotTo(gomega.HaveOccurred())
258 gomega.Expect(len(u.Items)).To(gomega.Equal(count), "unexpected number of %s", obj.GetKind())
259 }
260
261 func RandomString(prefix string) string {
262 randomSuffix := common.RandomStr()
263 return fmt.Sprintf("%s%s", prefix, randomSuffix)
264 }
265
266 func Run(ch <-chan event.Event) error {
267 var err error
268 for e := range ch {
269 if e.Type == event.ErrorType {
270 err = e.ErrorEvent.Err
271 }
272 }
273 return err
274 }
275
276 var RunWithNoErr = RunCollectNoErr
277
278 func RunCollect(ch <-chan event.Event) []event.Event {
279 var events []event.Event
280 for e := range ch {
281 events = append(events, e)
282 }
283 return events
284 }
285
286 func RunCollectNoErr(ch <-chan event.Event, callerSkip ...int) []event.Event {
287 skip := 0
288 if len(callerSkip) > 0 {
289 skip = callerSkip[0]
290 }
291
292 events := RunCollect(ch)
293 ExpectNoEventErrors(events, skip+1)
294 ExpectNoReconcileTimeouts(events, skip+1)
295 return events
296 }
297
298 func ExpectNoEventErrors(events []event.Event, callerSkip ...int) {
299 skip := 0
300 if len(callerSkip) > 0 {
301 skip = callerSkip[0]
302 }
303
304 gomega.Expect(events).WithOffset(skip + 1).NotTo(
305 gomega.ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras,
306 gstruct.Fields{
307 "Type": gomega.Equal(event.ErrorType),
308 })))
309 gomega.Expect(events).WithOffset(skip + 1).NotTo(
310 gomega.ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras,
311 gstruct.Fields{
312 "Type": gomega.Equal(event.ApplyType),
313 "ApplyEvent": gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
314 "Status": gomega.Equal(event.ApplyFailed),
315 }),
316 })))
317 gomega.Expect(events).WithOffset(skip + 1).NotTo(
318 gomega.ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras,
319 gstruct.Fields{
320 "Type": gomega.Equal(event.PruneType),
321 "PruneEvent": gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
322 "Status": gomega.Equal(event.PruneFailed),
323 }),
324 })))
325 gomega.Expect(events).WithOffset(skip + 1).NotTo(
326 gomega.ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras,
327 gstruct.Fields{
328 "Type": gomega.Equal(event.DeleteType),
329 "DeleteEvent": gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
330 "Status": gomega.Equal(event.DeleteFailed),
331 }),
332 })))
333 }
334
335 func ExpectNoReconcileTimeouts(events []event.Event, callerSkip ...int) {
336 skip := 0
337 if len(callerSkip) > 0 {
338 skip = callerSkip[0]
339 }
340
341 gomega.Expect(events).WithOffset(skip + 1).NotTo(
342 gomega.ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras,
343 gstruct.Fields{
344 "Type": gomega.Equal(event.WaitType),
345 "WaitEvent": gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
346 "Status": gomega.Equal(event.ReconcileTimeout),
347 }),
348 })))
349 }
350
351 func ManifestToUnstructured(manifest []byte) *unstructured.Unstructured {
352 u := make(map[string]interface{})
353 err := yaml.Unmarshal(manifest, &u)
354 if err != nil {
355 panic(fmt.Errorf("failed to parse manifest yaml: %w", err))
356 }
357 return &unstructured.Unstructured{
358 Object: u,
359 }
360 }
361
362 func TemplateToUnstructured(tmpl string, data interface{}) *unstructured.Unstructured {
363 t, err := template.New("manifest").Parse(tmpl)
364 if err != nil {
365 panic(fmt.Errorf("failed to parse manifest go-template: %w", err))
366 }
367 var buffer bytes.Buffer
368 err = t.Execute(&buffer, data)
369 if err != nil {
370 panic(fmt.Errorf("failed to execute manifest go-template: %w", err))
371 }
372 return ManifestToUnstructured(buffer.Bytes())
373 }
374
375 func CreateInventoryCRD(ctx context.Context, c client.Client) {
376 invCRD := ManifestToUnstructured(customprovider.InventoryCRD)
377 var u unstructured.Unstructured
378 u.SetGroupVersionKind(invCRD.GroupVersionKind())
379 err := c.Get(ctx, types.NamespacedName{
380 Name: invCRD.GetName(),
381 }, &u)
382 if apierrors.IsNotFound(err) {
383 err = c.Create(ctx, invCRD)
384 }
385 gomega.Expect(err).NotTo(gomega.HaveOccurred())
386 }
387
388 func CreateRandomNamespace(ctx context.Context, c client.Client) *v1.Namespace {
389 namespaceName := RandomString("e2e-test-")
390 namespace := &v1.Namespace{
391 TypeMeta: metav1.TypeMeta{
392 APIVersion: v1.SchemeGroupVersion.String(),
393 Kind: "Namespace",
394 },
395 ObjectMeta: metav1.ObjectMeta{
396 Name: namespaceName,
397 },
398 }
399
400 err := c.Create(ctx, namespace)
401 gomega.Expect(err).ToNot(gomega.HaveOccurred())
402 return namespace
403 }
404
405 func DeleteInventoryCRD(ctx context.Context, c client.Client) {
406 invCRD := ManifestToUnstructured(customprovider.InventoryCRD)
407 DeleteUnstructuredIfExists(ctx, c, invCRD)
408 }
409
410 func DeleteUnstructuredIfExists(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
411 err := c.Delete(ctx, obj)
412 if err != nil {
413 expectNotFoundError(err)
414 }
415 }
416
417 func DeleteAllUnstructuredIfExists(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
418 err := c.DeleteAllOf(ctx, obj,
419 client.InNamespace(obj.GetNamespace()),
420 client.MatchingLabels(obj.GetLabels()))
421 if err != nil {
422 expectNotFoundError(err)
423 }
424 }
425
426 func DeleteNamespace(ctx context.Context, c client.Client, namespace *v1.Namespace) {
427 err := c.Delete(ctx, namespace)
428 gomega.Expect(err).ToNot(gomega.HaveOccurred())
429 }
430
431 func UnstructuredExistsAndIsNotTerminating(ctx context.Context, c client.Client, obj *unstructured.Unstructured) bool {
432 serverObj := obj.DeepCopy()
433 err := c.Get(ctx, types.NamespacedName{
434 Namespace: obj.GetNamespace(),
435 Name: obj.GetName(),
436 }, serverObj)
437 if err != nil {
438 expectNotFoundError(err)
439 return false
440 }
441 return !UnstructuredIsTerminating(serverObj)
442 }
443
444 func expectNotFoundError(err error) {
445 gomega.Expect(err).To(gomega.Or(
446 gomega.BeAssignableToTypeOf(&meta.NoKindMatchError{}),
447 gomega.BeAssignableToTypeOf(&apierrors.StatusError{}),
448 ))
449 if se, ok := err.(*apierrors.StatusError); ok {
450 gomega.Expect(se.ErrStatus.Reason).To(gomega.Or(
451 gomega.Equal(metav1.StatusReasonNotFound),
452
453 gomega.Equal(metav1.StatusReasonMethodNotAllowed),
454 ))
455 }
456 }
457
458 func UnstructuredIsTerminating(obj *unstructured.Unstructured) bool {
459 objc, err := status.GetObjectWithConditions(obj.Object)
460 gomega.Expect(err).NotTo(gomega.HaveOccurred())
461 for _, c := range objc.Status.Conditions {
462 if c.Type == "Terminating" && c.Status == "True" {
463 return true
464 }
465 }
466 return false
467 }
468
469 func UnstructuredNamespace(name string) *unstructured.Unstructured {
470 u := &unstructured.Unstructured{}
471 u.SetAPIVersion("v1")
472 u.SetKind("Namespace")
473 u.SetName(name)
474 return u
475 }
476
477 func IsFlowControlEnabled(config *rest.Config) bool {
478 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
479 defer cancel()
480
481 enabled, err := flowcontrol.IsEnabled(ctx, config)
482 gomega.Expect(err).ToNot(gomega.HaveOccurred())
483
484 return enabled
485 }
486
487
488
489
490
491
492
493 func FilterOptionalEvents(expected, received []testutil.ExpEvent) ([]testutil.ExpEvent, []testutil.ExpEvent) {
494 expectedCopy := make([]testutil.ExpEvent, 0, len(expected))
495 for _, ee := range expected {
496 if ee.EventType == event.WaitType &&
497 ee.WaitEvent != nil &&
498 ee.WaitEvent.Status == event.ReconcilePending {
499
500
501 for i, re := range received {
502 if cmp.Equal(re, ee, cmpopts.EquateErrors()) {
503
504 received = append(received[:i], received[i+1:]...)
505 break
506 }
507 }
508 } else {
509 expectedCopy = append(expectedCopy, ee)
510 }
511 }
512 return expectedCopy, received
513 }
514
View as plain text