github.com/argoproj/argo-cd/v3@v3.2.1/test/e2e/fixture/applicationsets/expectation.go (about)

     1  package applicationsets
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/argoproj/gitops-engine/pkg/diff"
    10  	corev1 "k8s.io/api/core/v1"
    11  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    12  
    13  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    14  	"github.com/argoproj/argo-cd/v3/test/e2e/fixture/applicationsets/utils"
    15  )
    16  
    17  type state = string
    18  
    19  const (
    20  	failed    = "failed"
    21  	pending   = "pending"
    22  	succeeded = "succeeded"
    23  )
    24  
    25  // Expectation returns succeeded on succes condition, or pending/failed on failure, along with
    26  // a message to describe the success/failure condition.
    27  type Expectation func(c *Consequences) (state state, message string)
    28  
    29  // Success asserts that the last command was successful
    30  func Success(message string) Expectation {
    31  	return func(c *Consequences) (state, string) {
    32  		if c.actions.lastError != nil {
    33  			return failed, fmt.Sprintf("error: %v", c.actions.lastError)
    34  		}
    35  		if !strings.Contains(c.actions.lastOutput, message) {
    36  			return failed, fmt.Sprintf("output did not contain '%s'", message)
    37  		}
    38  		return succeeded, fmt.Sprintf("no error and output contained '%s'", message)
    39  	}
    40  }
    41  
    42  // Error asserts that the last command was an error with substring match
    43  func Error(message, err string) Expectation {
    44  	return func(c *Consequences) (state, string) {
    45  		if c.actions.lastError == nil {
    46  			return failed, "no error"
    47  		}
    48  		if !strings.Contains(c.actions.lastOutput, message) {
    49  			return failed, fmt.Sprintf("output does not contain '%s'", message)
    50  		}
    51  		if !strings.Contains(c.actions.lastError.Error(), err) {
    52  			return failed, fmt.Sprintf("error does not contain '%s'", message)
    53  		}
    54  		return succeeded, fmt.Sprintf("error '%s'", message)
    55  	}
    56  }
    57  
    58  // ApplicationsExist checks whether each of the 'expectedApps' exist in the namespace, and are
    59  // equivalent to provided values.
    60  func ApplicationsExist(expectedApps []v1alpha1.Application) Expectation {
    61  	return func(c *Consequences) (state, string) {
    62  		for _, expectedApp := range expectedApps {
    63  			foundApp := c.app(expectedApp.Name)
    64  			if foundApp == nil {
    65  				return pending, fmt.Sprintf("missing app '%s'", expectedApp.QualifiedName())
    66  			}
    67  
    68  			if !appsAreEqual(expectedApp, *foundApp) {
    69  				diff, err := getDiff(filterFields(expectedApp), filterFields(*foundApp))
    70  				if err != nil {
    71  					return failed, err.Error()
    72  				}
    73  
    74  				return pending, fmt.Sprintf("apps are not equal: '%s', diff: %s\n", expectedApp.QualifiedName(), diff)
    75  			}
    76  		}
    77  
    78  		return succeeded, "all apps successfully found"
    79  	}
    80  }
    81  
    82  // ApplicationSetHasConditions checks whether each of the 'expectedConditions' exist in the ApplicationSet status, and are
    83  // equivalent to provided values.
    84  func ApplicationSetHasConditions(applicationSetName string, expectedConditions []v1alpha1.ApplicationSetCondition) Expectation {
    85  	return func(c *Consequences) (state, string) {
    86  		// retrieve the application set
    87  		foundApplicationSet := c.applicationSet(applicationSetName)
    88  		if foundApplicationSet == nil {
    89  			return pending, fmt.Sprintf("application set '%s' not found", applicationSetName)
    90  		}
    91  
    92  		if !conditionsAreEqual(&expectedConditions, &foundApplicationSet.Status.Conditions) {
    93  			diff, err := getConditionDiff(expectedConditions, foundApplicationSet.Status.Conditions)
    94  			if err != nil {
    95  				return failed, err.Error()
    96  			}
    97  			return pending, fmt.Sprintf("application set conditions are not equal: '%s', diff: %s\n", expectedConditions, diff)
    98  		}
    99  		return succeeded, "application set successfully found"
   100  	}
   101  }
   102  
   103  // ApplicationsDoNotExist checks that each of the 'expectedApps' no longer exist in the namespace
   104  func ApplicationsDoNotExist(expectedApps []v1alpha1.Application) Expectation {
   105  	return func(c *Consequences) (state, string) {
   106  		for _, expectedApp := range expectedApps {
   107  			foundApp := c.app(expectedApp.Name)
   108  			if foundApp != nil {
   109  				return pending, fmt.Sprintf("app '%s' should no longer exist", expectedApp.QualifiedName())
   110  			}
   111  		}
   112  
   113  		return succeeded, "all apps do not exist"
   114  	}
   115  }
   116  
   117  // Pod checks whether a specified condition is true for any of the pods in the namespace
   118  func Pod(t *testing.T, predicate func(p corev1.Pod) bool) Expectation {
   119  	t.Helper()
   120  	return func(_ *Consequences) (state, string) {
   121  		pods, err := pods(t, utils.ApplicationsResourcesNamespace)
   122  		if err != nil {
   123  			return failed, err.Error()
   124  		}
   125  		for _, pod := range pods.Items {
   126  			if predicate(pod) {
   127  				return succeeded, fmt.Sprintf("pod predicate matched pod named '%s'", pod.GetName())
   128  			}
   129  		}
   130  		return pending, "pod predicate does not match pods"
   131  	}
   132  }
   133  
   134  func pods(t *testing.T, namespace string) (*corev1.PodList, error) {
   135  	t.Helper()
   136  	fixtureClient := utils.GetE2EFixtureK8sClient(t)
   137  
   138  	pods, err := fixtureClient.KubeClientset.CoreV1().Pods(namespace).List(t.Context(), metav1.ListOptions{})
   139  	return pods, err
   140  }
   141  
   142  // getDiff returns a string containing a comparison result of two applications (for test output/debug purposes)
   143  func getDiff(orig, newApplication v1alpha1.Application) (string, error) {
   144  	bytes, _, err := diff.CreateTwoWayMergePatch(orig, newApplication, orig)
   145  	if err != nil {
   146  		return "", err
   147  	}
   148  
   149  	return string(bytes), nil
   150  }
   151  
   152  // getConditionDiff returns a string containing a comparison result of two ApplicationSetCondition (for test output/debug purposes)
   153  func getConditionDiff(orig, newApplicationSetCondition []v1alpha1.ApplicationSetCondition) (string, error) {
   154  	if len(orig) != len(newApplicationSetCondition) {
   155  		return fmt.Sprintf("mismatch between condition sizes: %v %v", len(orig), len(newApplicationSetCondition)), nil
   156  	}
   157  
   158  	var bytes []byte
   159  
   160  	for index := range orig {
   161  		b, _, err := diff.CreateTwoWayMergePatch(orig[index], newApplicationSetCondition[index], orig[index])
   162  		if err != nil {
   163  			return "", err
   164  		}
   165  		bytes = append(bytes, b...)
   166  	}
   167  
   168  	return string(bytes), nil
   169  }
   170  
   171  // filterFields returns a copy of Application, but with unnecessary (for testing) fields removed
   172  func filterFields(input v1alpha1.Application) v1alpha1.Application {
   173  	spec := input.Spec
   174  
   175  	metaCopy := input.ObjectMeta.DeepCopy()
   176  
   177  	output := v1alpha1.Application{
   178  		ObjectMeta: metav1.ObjectMeta{
   179  			Labels:      metaCopy.Labels,
   180  			Annotations: metaCopy.Annotations,
   181  			Name:        metaCopy.Name,
   182  			Namespace:   metaCopy.Namespace,
   183  			Finalizers:  metaCopy.Finalizers,
   184  		},
   185  		Spec: v1alpha1.ApplicationSpec{
   186  			Source: &v1alpha1.ApplicationSource{
   187  				Path:           spec.GetSource().Path,
   188  				RepoURL:        spec.GetSource().RepoURL,
   189  				TargetRevision: spec.GetSource().TargetRevision,
   190  			},
   191  			Destination: v1alpha1.ApplicationDestination{
   192  				Server:    spec.Destination.Server,
   193  				Name:      spec.Destination.Name,
   194  				Namespace: spec.Destination.Namespace,
   195  			},
   196  			Project: spec.Project,
   197  		},
   198  	}
   199  
   200  	return output
   201  }
   202  
   203  // filterConditionFields returns a copy of ApplicationSetCondition, but with unnecessary (for testing) fields removed
   204  func filterConditionFields(input *[]v1alpha1.ApplicationSetCondition) *[]v1alpha1.ApplicationSetCondition {
   205  	var filteredConditions []v1alpha1.ApplicationSetCondition
   206  	for _, condition := range *input {
   207  		newCondition := &v1alpha1.ApplicationSetCondition{
   208  			Type:    condition.Type,
   209  			Status:  condition.Status,
   210  			Message: condition.Message,
   211  			Reason:  condition.Reason,
   212  		}
   213  		filteredConditions = append(filteredConditions, *newCondition)
   214  	}
   215  
   216  	return &filteredConditions
   217  }
   218  
   219  // appsAreEqual returns true if the apps are equal, comparing only fields of interest
   220  func appsAreEqual(one v1alpha1.Application, two v1alpha1.Application) bool {
   221  	return reflect.DeepEqual(filterFields(one), filterFields(two))
   222  }
   223  
   224  // conditionsAreEqual returns true if the appset status conditions are equal, comparing only fields of interest
   225  func conditionsAreEqual(one, two *[]v1alpha1.ApplicationSetCondition) bool {
   226  	return reflect.DeepEqual(filterConditionFields(one), filterConditionFields(two))
   227  }