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

     1  package applicationsets
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  	"strings"
     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/v2/pkg/apis/application/v1alpha1"
    14  	"github.com/argoproj/argo-cd/v2/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  
    63  		for _, expectedApp := range expectedApps {
    64  			foundApp := c.app(expectedApp.Name)
    65  			if foundApp == nil {
    66  				return pending, fmt.Sprintf("missing app '%s'", expectedApp.QualifiedName())
    67  			}
    68  
    69  			if !appsAreEqual(expectedApp, *foundApp) {
    70  
    71  				diff, err := getDiff(filterFields(expectedApp), filterFields(*foundApp))
    72  				if err != nil {
    73  					return failed, err.Error()
    74  				}
    75  
    76  				return pending, fmt.Sprintf("apps are not equal: '%s', diff: %s\n", expectedApp.QualifiedName(), diff)
    77  
    78  			}
    79  
    80  		}
    81  
    82  		return succeeded, "all apps successfully found"
    83  	}
    84  }
    85  
    86  // ApplicationSetHasConditions checks whether each of the 'expectedConditions' exist in the ApplicationSet status, and are
    87  // equivalent to provided values.
    88  func ApplicationSetHasConditions(applicationSetName string, expectedConditions []v1alpha1.ApplicationSetCondition) Expectation {
    89  	return func(c *Consequences) (state, string) {
    90  
    91  		// retrieve the application set
    92  		foundApplicationSet := c.applicationSet(applicationSetName)
    93  		if foundApplicationSet == nil {
    94  			return pending, fmt.Sprintf("application set '%s' not found", applicationSetName)
    95  		}
    96  
    97  		if !conditionsAreEqual(&expectedConditions, &foundApplicationSet.Status.Conditions) {
    98  			diff, err := getConditionDiff(expectedConditions, foundApplicationSet.Status.Conditions)
    99  			if err != nil {
   100  				return failed, err.Error()
   101  			}
   102  			return pending, fmt.Sprintf("application set conditions are not equal: '%s', diff: %s\n", expectedConditions, diff)
   103  		}
   104  		return succeeded, "application set successfully found"
   105  	}
   106  }
   107  
   108  // ApplicationsDoNotExist checks that each of the 'expectedApps' no longer exist in the namespace
   109  func ApplicationsDoNotExist(expectedApps []v1alpha1.Application) Expectation {
   110  	return func(c *Consequences) (state, string) {
   111  
   112  		for _, expectedApp := range expectedApps {
   113  			foundApp := c.app(expectedApp.Name)
   114  			if foundApp != nil {
   115  				return pending, fmt.Sprintf("app '%s' should no longer exist", expectedApp.QualifiedName())
   116  			}
   117  		}
   118  
   119  		return succeeded, "all apps do not exist"
   120  	}
   121  }
   122  
   123  // Pod checks whether a specified condition is true for any of the pods in the namespace
   124  func Pod(predicate func(p corev1.Pod) bool) Expectation {
   125  	return func(c *Consequences) (state, string) {
   126  		pods, err := pods(utils.ApplicationsResourcesNamespace)
   127  		if err != nil {
   128  			return failed, err.Error()
   129  		}
   130  		for _, pod := range pods.Items {
   131  			if predicate(pod) {
   132  				return succeeded, fmt.Sprintf("pod predicate matched pod named '%s'", pod.GetName())
   133  			}
   134  		}
   135  		return pending, "pod predicate does not match pods"
   136  	}
   137  }
   138  
   139  func pods(namespace string) (*corev1.PodList, error) {
   140  	fixtureClient := utils.GetE2EFixtureK8sClient()
   141  
   142  	pods, err := fixtureClient.KubeClientset.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{})
   143  	return pods, err
   144  }
   145  
   146  // getDiff returns a string containing a comparison result of two applications (for test output/debug purposes)
   147  func getDiff(orig, new v1alpha1.Application) (string, error) {
   148  
   149  	bytes, _, err := diff.CreateTwoWayMergePatch(orig, new, orig)
   150  	if err != nil {
   151  		return "", err
   152  	}
   153  
   154  	return string(bytes), nil
   155  
   156  }
   157  
   158  // getConditionDiff returns a string containing a comparison result of two ApplicationSetCondition (for test output/debug purposes)
   159  func getConditionDiff(orig, new []v1alpha1.ApplicationSetCondition) (string, error) {
   160  	if len(orig) != len(new) {
   161  		return fmt.Sprintf("mismatch between condition sizes: %v %v", len(orig), len(new)), nil
   162  	}
   163  
   164  	var bytes []byte
   165  
   166  	for index := range orig {
   167  		b, _, err := diff.CreateTwoWayMergePatch(orig[index], new[index], orig[index])
   168  		if err != nil {
   169  			return "", err
   170  		}
   171  		bytes = append(bytes, b...)
   172  	}
   173  
   174  	return string(bytes), nil
   175  
   176  }
   177  
   178  // filterFields returns a copy of Application, but with unnecessary (for testing) fields removed
   179  func filterFields(input v1alpha1.Application) v1alpha1.Application {
   180  
   181  	spec := input.Spec
   182  
   183  	metaCopy := input.ObjectMeta.DeepCopy()
   184  
   185  	output := v1alpha1.Application{
   186  		ObjectMeta: metav1.ObjectMeta{
   187  			Labels:      metaCopy.Labels,
   188  			Annotations: metaCopy.Annotations,
   189  			Name:        metaCopy.Name,
   190  			Namespace:   metaCopy.Namespace,
   191  			Finalizers:  metaCopy.Finalizers,
   192  		},
   193  		Spec: v1alpha1.ApplicationSpec{
   194  			Source: &v1alpha1.ApplicationSource{
   195  				Path:           spec.GetSource().Path,
   196  				RepoURL:        spec.GetSource().RepoURL,
   197  				TargetRevision: spec.GetSource().TargetRevision,
   198  			},
   199  			Destination: v1alpha1.ApplicationDestination{
   200  				Server:    spec.Destination.Server,
   201  				Name:      spec.Destination.Name,
   202  				Namespace: spec.Destination.Namespace,
   203  			},
   204  			Project: spec.Project,
   205  		},
   206  	}
   207  
   208  	return output
   209  }
   210  
   211  // filterConditionFields returns a copy of ApplicationSetCondition, but with unnecessary (for testing) fields removed
   212  func filterConditionFields(input *[]v1alpha1.ApplicationSetCondition) *[]v1alpha1.ApplicationSetCondition {
   213  
   214  	var filteredConditions []v1alpha1.ApplicationSetCondition
   215  	for _, condition := range *input {
   216  		newCondition := &v1alpha1.ApplicationSetCondition{
   217  			Type:    condition.Type,
   218  			Status:  condition.Status,
   219  			Message: condition.Message,
   220  			Reason:  condition.Reason,
   221  		}
   222  		filteredConditions = append(filteredConditions, *newCondition)
   223  	}
   224  
   225  	return &filteredConditions
   226  }
   227  
   228  // appsAreEqual returns true if the apps are equal, comparing only fields of interest
   229  func appsAreEqual(one v1alpha1.Application, two v1alpha1.Application) bool {
   230  	return reflect.DeepEqual(filterFields(one), filterFields(two))
   231  }
   232  
   233  // conditionsAreEqual returns true if the appset status conditions are equal, comparing only fields of interest
   234  func conditionsAreEqual(one, two *[]v1alpha1.ApplicationSetCondition) bool {
   235  	return reflect.DeepEqual(filterConditionFields(one), filterConditionFields(two))
   236  }