mirror of https://github.com/fluxcd/cli-utils.git
405 lines
12 KiB
Go
405 lines
12 KiB
Go
// Copyright 2020 The Kubernetes Authors.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package e2eutil
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/onsi/ginkgo"
|
|
"github.com/onsi/gomega"
|
|
v1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/yaml"
|
|
"sigs.k8s.io/cli-utils/pkg/apply/event"
|
|
"sigs.k8s.io/cli-utils/pkg/common"
|
|
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
|
|
"sigs.k8s.io/cli-utils/pkg/object/dependson"
|
|
"sigs.k8s.io/cli-utils/pkg/object/mutation"
|
|
"sigs.k8s.io/cli-utils/test/e2e/customprovider"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
)
|
|
|
|
func WithReplicas(obj *unstructured.Unstructured, replicas int) *unstructured.Unstructured {
|
|
err := unstructured.SetNestedField(obj.Object, int64(replicas), "spec", "replicas")
|
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
return obj
|
|
}
|
|
|
|
func WithNamespace(obj *unstructured.Unstructured, namespace string) *unstructured.Unstructured {
|
|
obj.SetNamespace(namespace)
|
|
return obj
|
|
}
|
|
|
|
func PodWithImage(obj *unstructured.Unstructured, containerName, image string) *unstructured.Unstructured {
|
|
containers, found, err := unstructured.NestedSlice(obj.Object, "spec", "containers")
|
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
gomega.Expect(found).To(gomega.BeTrue())
|
|
|
|
containerFound := false
|
|
for i := range containers {
|
|
container := containers[i].(map[string]interface{})
|
|
name := container["name"].(string)
|
|
if name != containerName {
|
|
continue
|
|
}
|
|
containerFound = true
|
|
container["image"] = image
|
|
}
|
|
gomega.Expect(containerFound).To(gomega.BeTrue())
|
|
err = unstructured.SetNestedSlice(obj.Object, containers, "spec", "containers")
|
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
return obj
|
|
}
|
|
|
|
func WithNodeSelector(obj *unstructured.Unstructured, key, value string) *unstructured.Unstructured {
|
|
selectors, found, err := unstructured.NestedMap(obj.Object, "spec", "nodeSelector")
|
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
|
|
if !found {
|
|
selectors = make(map[string]interface{})
|
|
}
|
|
selectors[key] = value
|
|
err = unstructured.SetNestedMap(obj.Object, selectors, "spec", "nodeSelector")
|
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
return obj
|
|
}
|
|
|
|
func WithAnnotation(obj *unstructured.Unstructured, key, value string) *unstructured.Unstructured {
|
|
annotations := obj.GetAnnotations()
|
|
if annotations == nil {
|
|
annotations = make(map[string]string)
|
|
}
|
|
annotations[key] = value
|
|
obj.SetAnnotations(annotations)
|
|
return obj
|
|
}
|
|
|
|
func WithDependsOn(obj *unstructured.Unstructured, dep string) *unstructured.Unstructured {
|
|
a := obj.GetAnnotations()
|
|
if a == nil {
|
|
a = make(map[string]string, 1)
|
|
}
|
|
a[dependson.Annotation] = dep
|
|
obj.SetAnnotations(a)
|
|
return obj
|
|
}
|
|
|
|
func DeleteUnstructuredAndWait(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
|
|
ref := mutation.ResourceReferenceFromUnstructured(obj)
|
|
|
|
err := c.Delete(ctx, obj,
|
|
client.PropagationPolicy(metav1.DeletePropagationForeground))
|
|
gomega.Expect(err).NotTo(gomega.HaveOccurred(),
|
|
"expected DELETE to not error (%s): %s", ref, err)
|
|
|
|
WaitForDeletion(ctx, c, obj)
|
|
}
|
|
|
|
func WaitForDeletion(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
|
|
ref := mutation.ResourceReferenceFromUnstructured(obj)
|
|
resultObj := ref.ToUnstructured()
|
|
|
|
timeout := 30 * time.Second
|
|
retry := 2 * time.Second
|
|
|
|
t := time.NewTimer(timeout)
|
|
s := time.NewTimer(0)
|
|
defer t.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-t.C:
|
|
ginkgo.Fail("timed out waiting for resource to be fully deleted")
|
|
return
|
|
case <-s.C:
|
|
err := c.Get(ctx, types.NamespacedName{
|
|
Namespace: obj.GetNamespace(),
|
|
Name: obj.GetName(),
|
|
}, resultObj)
|
|
if err != nil {
|
|
gomega.Expect(apierrors.ReasonForError(err)).To(gomega.Equal(metav1.StatusReasonNotFound),
|
|
"expected GET to error with NotFound (%s): %s", ref, err)
|
|
return
|
|
}
|
|
s = time.NewTimer(retry)
|
|
}
|
|
}
|
|
}
|
|
|
|
func CreateUnstructuredAndWait(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
|
|
ref := mutation.ResourceReferenceFromUnstructured(obj)
|
|
|
|
err := c.Create(ctx, obj)
|
|
gomega.Expect(err).NotTo(gomega.HaveOccurred(),
|
|
"expected CREATE to not error (%s): %s", ref, err)
|
|
|
|
WaitForCreation(ctx, c, obj)
|
|
}
|
|
|
|
func WaitForCreation(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
|
|
ref := mutation.ResourceReferenceFromUnstructured(obj)
|
|
resultObj := ref.ToUnstructured()
|
|
|
|
timeout := 30 * time.Second
|
|
retry := 2 * time.Second
|
|
|
|
t := time.NewTimer(timeout)
|
|
s := time.NewTimer(0)
|
|
defer t.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-t.C:
|
|
ginkgo.Fail("timed out waiting for resource to be fully created")
|
|
return
|
|
case <-s.C:
|
|
err := c.Get(ctx, types.NamespacedName{
|
|
Namespace: obj.GetNamespace(),
|
|
Name: obj.GetName(),
|
|
}, resultObj)
|
|
if err == nil {
|
|
return
|
|
}
|
|
gomega.Expect(apierrors.ReasonForError(err)).To(gomega.Equal(metav1.StatusReasonNotFound),
|
|
"expected GET to error with NotFound (%s): %s", ref, err)
|
|
// if NotFound, sleep and retry
|
|
s = time.NewTimer(retry)
|
|
}
|
|
}
|
|
}
|
|
|
|
func AssertUnstructuredExists(ctx context.Context, c client.Client, obj *unstructured.Unstructured) *unstructured.Unstructured {
|
|
ref := mutation.ResourceReferenceFromUnstructured(obj)
|
|
resultObj := ref.ToUnstructured()
|
|
|
|
err := c.Get(ctx, types.NamespacedName{
|
|
Namespace: obj.GetNamespace(),
|
|
Name: obj.GetName(),
|
|
}, resultObj)
|
|
gomega.Expect(err).NotTo(gomega.HaveOccurred(),
|
|
"expected GET not to error (%s): %s", ref, err)
|
|
return resultObj
|
|
}
|
|
|
|
func AssertUnstructuredDoesNotExist(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
|
|
ref := mutation.ResourceReferenceFromUnstructured(obj)
|
|
resultObj := ref.ToUnstructured()
|
|
|
|
err := c.Get(ctx, types.NamespacedName{
|
|
Namespace: obj.GetNamespace(),
|
|
Name: obj.GetName(),
|
|
}, resultObj)
|
|
gomega.Expect(err).To(gomega.HaveOccurred(),
|
|
"expected GET to error (%s)", ref)
|
|
gomega.Expect(apierrors.ReasonForError(err)).To(gomega.Equal(metav1.StatusReasonNotFound),
|
|
"expected GET to error with NotFound (%s): %s", ref, err)
|
|
}
|
|
|
|
func ApplyUnstructured(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
|
|
ref := mutation.ResourceReferenceFromUnstructured(obj)
|
|
resultObj := ref.ToUnstructured()
|
|
|
|
err := c.Get(ctx, types.NamespacedName{
|
|
Namespace: obj.GetNamespace(),
|
|
Name: obj.GetName(),
|
|
}, resultObj)
|
|
gomega.Expect(err).NotTo(gomega.HaveOccurred(),
|
|
"expected GET not to error (%s)", ref)
|
|
|
|
err = c.Patch(ctx, obj, client.MergeFrom(resultObj))
|
|
gomega.Expect(err).NotTo(gomega.HaveOccurred(),
|
|
"expected PATCH not to error (%s): %s", ref, err)
|
|
}
|
|
|
|
func AssertUnstructuredAvailable(obj *unstructured.Unstructured) {
|
|
ref := mutation.ResourceReferenceFromUnstructured(obj)
|
|
objc, err := status.GetObjectWithConditions(obj.Object)
|
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
available := false
|
|
for _, c := range objc.Status.Conditions {
|
|
// appsv1.DeploymentAvailable && corev1.ConditionTrue
|
|
if c.Type == "Available" && c.Status == "True" {
|
|
available = true
|
|
break
|
|
}
|
|
}
|
|
gomega.Expect(available).To(gomega.BeTrue(),
|
|
"expected Available condition to be True (%s)", ref)
|
|
}
|
|
|
|
func AssertUnstructuredCount(ctx context.Context, c client.Client, obj *unstructured.Unstructured, count int) {
|
|
var u unstructured.UnstructuredList
|
|
u.SetGroupVersionKind(obj.GetObjectKind().GroupVersionKind())
|
|
err := c.List(ctx, &u,
|
|
client.InNamespace(obj.GetNamespace()),
|
|
client.MatchingLabels(obj.GetLabels()))
|
|
if err != nil && count == 0 {
|
|
expectNotFoundError(err)
|
|
return
|
|
}
|
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
gomega.Expect(len(u.Items)).To(gomega.Equal(count), "unexpected number of %s", obj.GetKind())
|
|
}
|
|
|
|
func RandomString(prefix string) string {
|
|
randomSuffix := common.RandomStr()
|
|
return fmt.Sprintf("%s%s", prefix, randomSuffix)
|
|
}
|
|
|
|
func Run(ch <-chan event.Event) error {
|
|
var err error
|
|
for e := range ch {
|
|
if e.Type == event.ErrorType {
|
|
err = e.ErrorEvent.Err
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func RunWithNoErr(ch <-chan event.Event) {
|
|
RunCollectNoErr(ch)
|
|
}
|
|
|
|
func RunCollect(ch <-chan event.Event) []event.Event {
|
|
var events []event.Event
|
|
for e := range ch {
|
|
events = append(events, e)
|
|
}
|
|
return events
|
|
}
|
|
|
|
func RunCollectNoErr(ch <-chan event.Event) []event.Event {
|
|
events := RunCollect(ch)
|
|
for _, e := range events {
|
|
gomega.Expect(e.Type).NotTo(gomega.Equal(event.ErrorType))
|
|
}
|
|
return events
|
|
}
|
|
|
|
func ManifestToUnstructured(manifest []byte) *unstructured.Unstructured {
|
|
u := make(map[string]interface{})
|
|
err := yaml.Unmarshal(manifest, &u)
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to parse manifest yaml: %w", err))
|
|
}
|
|
return &unstructured.Unstructured{
|
|
Object: u,
|
|
}
|
|
}
|
|
|
|
func TemplateToUnstructured(tmpl string, data interface{}) *unstructured.Unstructured {
|
|
t, err := template.New("manifest").Parse(tmpl)
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to parse manifest go-template: %w", err))
|
|
}
|
|
var buffer bytes.Buffer
|
|
err = t.Execute(&buffer, data)
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to execute manifest go-template: %w", err))
|
|
}
|
|
return ManifestToUnstructured(buffer.Bytes())
|
|
}
|
|
|
|
func CreateInventoryCRD(ctx context.Context, c client.Client) {
|
|
invCRD := ManifestToUnstructured(customprovider.InventoryCRD)
|
|
var u unstructured.Unstructured
|
|
u.SetGroupVersionKind(invCRD.GroupVersionKind())
|
|
err := c.Get(ctx, types.NamespacedName{
|
|
Name: invCRD.GetName(),
|
|
}, &u)
|
|
if apierrors.IsNotFound(err) {
|
|
err = c.Create(ctx, invCRD)
|
|
}
|
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
}
|
|
|
|
func CreateRandomNamespace(ctx context.Context, c client.Client) *v1.Namespace {
|
|
namespaceName := RandomString("e2e-test-")
|
|
namespace := &v1.Namespace{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: v1.SchemeGroupVersion.String(),
|
|
Kind: "Namespace",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: namespaceName,
|
|
},
|
|
}
|
|
|
|
err := c.Create(ctx, namespace)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
return namespace
|
|
}
|
|
|
|
func DeleteInventoryCRD(ctx context.Context, c client.Client) {
|
|
invCRD := ManifestToUnstructured(customprovider.InventoryCRD)
|
|
DeleteUnstructuredIfExists(ctx, c, invCRD)
|
|
}
|
|
|
|
func DeleteUnstructuredIfExists(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
|
|
err := c.Delete(ctx, obj)
|
|
if err != nil {
|
|
expectNotFoundError(err)
|
|
}
|
|
}
|
|
|
|
func DeleteAllUnstructuredIfExists(ctx context.Context, c client.Client, obj *unstructured.Unstructured) {
|
|
err := c.DeleteAllOf(ctx, obj,
|
|
client.InNamespace(obj.GetNamespace()),
|
|
client.MatchingLabels(obj.GetLabels()))
|
|
if err != nil {
|
|
expectNotFoundError(err)
|
|
}
|
|
}
|
|
|
|
func DeleteNamespace(ctx context.Context, c client.Client, namespace *v1.Namespace) {
|
|
err := c.Delete(ctx, namespace)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
}
|
|
|
|
func UnstructuredExistsAndIsNotTerminating(ctx context.Context, c client.Client, obj *unstructured.Unstructured) bool {
|
|
serverObj := obj.DeepCopy()
|
|
err := c.Get(ctx, types.NamespacedName{
|
|
Namespace: obj.GetNamespace(),
|
|
Name: obj.GetName(),
|
|
}, serverObj)
|
|
if err != nil {
|
|
expectNotFoundError(err)
|
|
return false
|
|
}
|
|
return !UnstructuredIsTerminating(serverObj)
|
|
}
|
|
|
|
func expectNotFoundError(err error) {
|
|
gomega.Expect(err).To(gomega.Or(
|
|
gomega.BeAssignableToTypeOf(&meta.NoKindMatchError{}),
|
|
gomega.BeAssignableToTypeOf(&apierrors.StatusError{}),
|
|
))
|
|
if se, ok := err.(*apierrors.StatusError); ok {
|
|
gomega.Expect(se.ErrStatus.Reason).To(gomega.Or(
|
|
gomega.Equal(metav1.StatusReasonNotFound),
|
|
// custom resources dissalow deletion if the CRD is terminating
|
|
gomega.Equal(metav1.StatusReasonMethodNotAllowed),
|
|
))
|
|
}
|
|
}
|
|
|
|
func UnstructuredIsTerminating(obj *unstructured.Unstructured) bool {
|
|
objc, err := status.GetObjectWithConditions(obj.Object)
|
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
for _, c := range objc.Status.Conditions {
|
|
if c.Type == "Terminating" && c.Status == "True" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|