...

Source file src/sigs.k8s.io/cli-utils/test/e2e/e2eutil/common.go

Documentation: sigs.k8s.io/cli-utils/test/e2e/e2eutil

     1  // Copyright 2020 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     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  			// if NotFound, sleep and retry
   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  		// appsv1.DeploymentAvailable && corev1.ConditionTrue
   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  			// custom resources dissalow deletion if the CRD is terminating
   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  // FilterOptionalEvents looks for optional events in the expected list and
   488  // removes them from both lists. This allows the output to be compared for
   489  // equality.
   490  //
   491  // Optional events include:
   492  // - WaitEvent with ReconcilePending
   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  			// Pending WaitEvent is optional.
   500  			// Remove first event match, if exists.
   501  			for i, re := range received {
   502  				if cmp.Equal(re, ee, cmpopts.EquateErrors()) {
   503  					// remove event at index i
   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