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

     1  package app
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  	"regexp"
     8  	"strings"
     9  
    10  	"github.com/argoproj/gitops-engine/pkg/health"
    11  	"github.com/argoproj/gitops-engine/pkg/sync/common"
    12  	corev1 "k8s.io/api/core/v1"
    13  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	"k8s.io/apimachinery/pkg/fields"
    16  
    17  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    18  	"github.com/argoproj/argo-cd/v3/test/e2e/fixture"
    19  )
    20  
    21  type state = string
    22  
    23  const (
    24  	failed    = "failed"
    25  	pending   = "pending"
    26  	succeeded = "succeeded"
    27  )
    28  
    29  type Expectation func(c *Consequences) (state state, message string)
    30  
    31  func OperationPhaseIs(expected common.OperationPhase) Expectation {
    32  	return func(c *Consequences) (state, string) {
    33  		operationState := c.app().Status.OperationState
    34  		actual := common.OperationRunning
    35  		msg := ""
    36  		if operationState != nil {
    37  			actual = operationState.Phase
    38  			msg = operationState.Message
    39  		}
    40  		message := fmt.Sprintf("operation phase should be %s, is %s, message: '%s'", expected, actual, msg)
    41  		return simple(actual == expected, message)
    42  	}
    43  }
    44  
    45  func OperationMessageContains(text string) Expectation {
    46  	return func(c *Consequences) (state, string) {
    47  		operationState := c.app().Status.OperationState
    48  		actual := ""
    49  		if operationState != nil {
    50  			actual = operationState.Message
    51  		}
    52  		return simple(strings.Contains(actual, text), fmt.Sprintf("operation message should contains '%s', got: '%s'", text, actual))
    53  	}
    54  }
    55  
    56  func OperationRetriedMinimumTimes(minRetries int64) Expectation {
    57  	return func(c *Consequences) (state, string) {
    58  		operationState := c.app().Status.OperationState
    59  		actual := operationState.RetryCount
    60  		message := fmt.Sprintf("operation state retry cound should be at least %d, is %d, message: '%s'", minRetries, actual, operationState.Message)
    61  		return simple(actual >= minRetries, message)
    62  	}
    63  }
    64  
    65  func simple(success bool, message string) (state, string) {
    66  	if success {
    67  		return succeeded, message
    68  	}
    69  	return pending, message
    70  }
    71  
    72  func SyncStatusIs(expected v1alpha1.SyncStatusCode) Expectation {
    73  	return func(c *Consequences) (state, string) {
    74  		actual := c.app().Status.Sync.Status
    75  		return simple(actual == expected, fmt.Sprintf("sync status to be %s, is %s", expected, actual))
    76  	}
    77  }
    78  
    79  func HydrationPhaseIs(expected v1alpha1.HydrateOperationPhase) Expectation {
    80  	return func(c *Consequences) (state, string) {
    81  		actual := c.app().Status.SourceHydrator.CurrentOperation.Phase
    82  		return simple(actual == expected, fmt.Sprintf("hydration phase to be %s, is %s", expected, actual))
    83  	}
    84  }
    85  
    86  func Condition(conditionType v1alpha1.ApplicationConditionType, conditionMessage string) Expectation {
    87  	return func(c *Consequences) (state, string) {
    88  		got := c.app().Status.Conditions
    89  		message := fmt.Sprintf("condition {%s %s} in %v", conditionType, conditionMessage, got)
    90  		for _, condition := range got {
    91  			if conditionType == condition.Type && strings.Contains(condition.Message, conditionMessage) {
    92  				return succeeded, message
    93  			}
    94  		}
    95  		return pending, message
    96  	}
    97  }
    98  
    99  func NoConditions() Expectation {
   100  	return func(c *Consequences) (state, string) {
   101  		message := "no conditions"
   102  		if len(c.app().Status.Conditions) == 0 {
   103  			return succeeded, message
   104  		}
   105  		return pending, message
   106  	}
   107  }
   108  
   109  func NoStatus() Expectation {
   110  	return func(c *Consequences) (state, string) {
   111  		message := "no status"
   112  		if reflect.ValueOf(c.app().Status).IsZero() {
   113  			return succeeded, message
   114  		}
   115  		return pending, message
   116  	}
   117  }
   118  
   119  func StatusExists() Expectation {
   120  	return func(c *Consequences) (state, string) {
   121  		message := "status exists"
   122  		if !reflect.ValueOf(c.app().Status).IsZero() {
   123  			return succeeded, message
   124  		}
   125  		return pending, message
   126  	}
   127  }
   128  
   129  func Status(f func(v1alpha1.ApplicationStatus) (bool, string)) Expectation {
   130  	return func(c *Consequences) (state, string) {
   131  		ok, msg := f(c.app().Status)
   132  		if !ok {
   133  			return pending, msg
   134  		}
   135  		return succeeded, msg
   136  	}
   137  }
   138  
   139  func Namespace(name string, block func(app *v1alpha1.Application, ns *corev1.Namespace)) Expectation {
   140  	return func(c *Consequences) (state, string) {
   141  		ns, err := namespace(name)
   142  		if err != nil {
   143  			return failed, "namespace not found " + err.Error()
   144  		}
   145  
   146  		block(c.app(), ns)
   147  		return succeeded, fmt.Sprintf("namespace %s assertions passed", name)
   148  	}
   149  }
   150  
   151  func HealthIs(expected health.HealthStatusCode) Expectation {
   152  	return func(c *Consequences) (state, string) {
   153  		actual := c.app().Status.Health.Status
   154  		return simple(actual == expected, fmt.Sprintf("health to should %s, is %s", expected, actual))
   155  	}
   156  }
   157  
   158  func ResourceSyncStatusIs(kind, resource string, expected v1alpha1.SyncStatusCode) Expectation {
   159  	return func(c *Consequences) (state, string) {
   160  		actual := c.resource(kind, resource, "").Status
   161  		return simple(actual == expected, fmt.Sprintf("resource '%s/%s' sync status should be %s, is %s", kind, resource, expected, actual))
   162  	}
   163  }
   164  
   165  func ResourceSyncStatusWithNamespaceIs(kind, resource, namespace string, expected v1alpha1.SyncStatusCode) Expectation {
   166  	return func(c *Consequences) (state, string) {
   167  		actual := c.resource(kind, resource, namespace).Status
   168  		return simple(actual == expected, fmt.Sprintf("resource '%s/%s' sync status should be %s, is %s", kind, resource, expected, actual))
   169  	}
   170  }
   171  
   172  func ResourceHealthIs(kind, resource string, expected health.HealthStatusCode) Expectation {
   173  	return func(c *Consequences) (state, string) {
   174  		var actual health.HealthStatusCode
   175  		resourceHealth := c.resource(kind, resource, "").Health
   176  		if resourceHealth != nil {
   177  			actual = resourceHealth.Status
   178  		} else {
   179  			// Some resources like ConfigMap may not have health status when they are okay
   180  			actual = health.HealthStatusHealthy
   181  		}
   182  		return simple(actual == expected, fmt.Sprintf("resource '%s/%s' health should be %s, is %s", kind, resource, expected, actual))
   183  	}
   184  }
   185  
   186  func ResourceHealthWithNamespaceIs(kind, resource, namespace string, expected health.HealthStatusCode) Expectation {
   187  	return func(c *Consequences) (state, string) {
   188  		var actual health.HealthStatusCode
   189  		resourceHealth := c.resource(kind, resource, namespace).Health
   190  		if resourceHealth != nil {
   191  			actual = resourceHealth.Status
   192  		} else {
   193  			// Some resources like ConfigMap may not have health status when they are okay
   194  			actual = health.HealthStatusHealthy
   195  		}
   196  		return simple(actual == expected, fmt.Sprintf("resource '%s/%s' health should be %s, is %s", kind, resource, expected, actual))
   197  	}
   198  }
   199  
   200  func ResourceResultNumbering(num int) Expectation {
   201  	return func(c *Consequences) (state, string) {
   202  		actualNum := len(c.app().Status.OperationState.SyncResult.Resources)
   203  		if actualNum < num {
   204  			return pending, fmt.Sprintf("not enough results yet, want %d, got %d", num, actualNum)
   205  		} else if actualNum == num {
   206  			return succeeded, fmt.Sprintf("right number of results, want %d, got %d", num, actualNum)
   207  		}
   208  		return failed, fmt.Sprintf("too many results, want %d, got %d", num, actualNum)
   209  	}
   210  }
   211  
   212  func ResourceResultIs(result v1alpha1.ResourceResult) Expectation {
   213  	return func(c *Consequences) (state, string) {
   214  		results := c.app().Status.OperationState.SyncResult.Resources
   215  		for _, res := range results {
   216  			if reflect.DeepEqual(*res, result) {
   217  				return succeeded, fmt.Sprintf("found resource result %v", result)
   218  			}
   219  		}
   220  		return pending, fmt.Sprintf("waiting for resource result %v in %v", result, results)
   221  	}
   222  }
   223  
   224  func sameResourceResult(res1, res2 v1alpha1.ResourceResult) bool {
   225  	return res1.Kind == res2.Kind &&
   226  		res1.Group == res2.Group &&
   227  		res1.Namespace == res2.Namespace &&
   228  		res1.Name == res2.Name &&
   229  		res1.SyncPhase == res2.SyncPhase &&
   230  		res1.Status == res2.Status &&
   231  		res1.HookPhase == res2.HookPhase
   232  }
   233  
   234  func ResourceResultMatches(result v1alpha1.ResourceResult) Expectation {
   235  	return func(c *Consequences) (state, string) {
   236  		results := c.app().Status.OperationState.SyncResult.Resources
   237  		for _, res := range results {
   238  			if sameResourceResult(*res, result) {
   239  				re := regexp.MustCompile(result.Message)
   240  				if re.MatchString(res.Message) {
   241  					return succeeded, fmt.Sprintf("found resource result %v", result)
   242  				}
   243  			}
   244  		}
   245  		return pending, fmt.Sprintf("waiting for resource result %v in %v", result, results)
   246  	}
   247  }
   248  
   249  func DoesNotExist() Expectation {
   250  	return func(c *Consequences) (state, string) {
   251  		_, err := c.get()
   252  		if err != nil {
   253  			if apierrors.IsNotFound(err) {
   254  				return succeeded, "app does not exist"
   255  			}
   256  			return failed, err.Error()
   257  		}
   258  		return pending, "app should not exist"
   259  	}
   260  }
   261  
   262  func DoesNotExistNow() Expectation {
   263  	return func(c *Consequences) (state, string) {
   264  		_, err := c.get()
   265  		if err != nil {
   266  			if apierrors.IsNotFound(err) {
   267  				return succeeded, "app does not exist"
   268  			}
   269  			return failed, err.Error()
   270  		}
   271  		return failed, "app should not exist"
   272  	}
   273  }
   274  
   275  func Pod(predicate func(p corev1.Pod) bool) Expectation {
   276  	return func(_ *Consequences) (state, string) {
   277  		pods, err := pods()
   278  		if err != nil {
   279  			return failed, err.Error()
   280  		}
   281  		for _, pod := range pods.Items {
   282  			if predicate(pod) {
   283  				return succeeded, fmt.Sprintf("pod predicate matched pod named '%s'", pod.GetName())
   284  			}
   285  		}
   286  		return pending, "pod predicate does not match pods"
   287  	}
   288  }
   289  
   290  func NotPod(predicate func(p corev1.Pod) bool) Expectation {
   291  	return func(_ *Consequences) (state, string) {
   292  		pods, err := pods()
   293  		if err != nil {
   294  			return failed, err.Error()
   295  		}
   296  		for _, pod := range pods.Items {
   297  			if predicate(pod) {
   298  				return pending, fmt.Sprintf("pod predicate matched pod named '%s'", pod.GetName())
   299  			}
   300  		}
   301  		return succeeded, "pod predicate did not match any pod"
   302  	}
   303  }
   304  
   305  func pods() (*corev1.PodList, error) {
   306  	fixture.KubeClientset.CoreV1()
   307  	pods, err := fixture.KubeClientset.CoreV1().Pods(fixture.DeploymentNamespace()).List(context.Background(), metav1.ListOptions{})
   308  	return pods, err
   309  }
   310  
   311  func NoNamespace(name string) Expectation {
   312  	return func(_ *Consequences) (state, string) {
   313  		_, err := namespace(name)
   314  		if err != nil {
   315  			return succeeded, "namespace not found"
   316  		}
   317  
   318  		return failed, "found namespace " + name
   319  	}
   320  }
   321  
   322  func namespace(name string) (*corev1.Namespace, error) {
   323  	fixture.KubeClientset.CoreV1()
   324  	return fixture.KubeClientset.CoreV1().Namespaces().Get(context.Background(), name, metav1.GetOptions{})
   325  }
   326  
   327  func event(namespace string, reason string, message string) Expectation {
   328  	return func(c *Consequences) (state, string) {
   329  		list, err := fixture.KubeClientset.CoreV1().Events(namespace).List(context.Background(), metav1.ListOptions{
   330  			FieldSelector: fields.SelectorFromSet(map[string]string{
   331  				"involvedObject.name":      c.context.AppName(),
   332  				"involvedObject.namespace": namespace,
   333  			}).String(),
   334  		})
   335  		if err != nil {
   336  			return failed, err.Error()
   337  		}
   338  
   339  		for i := range list.Items {
   340  			event := list.Items[i]
   341  			if event.Reason == reason && strings.Contains(event.Message, message) {
   342  				return succeeded, fmt.Sprintf("found event with reason=%s; message=%s", reason, message)
   343  			}
   344  		}
   345  		return failed, fmt.Sprintf("unable to find event with reason=%s; message=%s", reason, message)
   346  	}
   347  }
   348  
   349  func Event(reason string, message string) Expectation {
   350  	return event(fixture.TestNamespace(), reason, message)
   351  }
   352  
   353  func NamespacedEvent(namespace string, reason string, message string) Expectation {
   354  	return event(namespace, reason, message)
   355  }
   356  
   357  // Success asserts that the last command was successful and that the output contains the given message.
   358  func Success(message string, matchers ...func(string, string) bool) Expectation {
   359  	if len(matchers) == 0 {
   360  		matchers = append(matchers, strings.Contains)
   361  	}
   362  	match := func(actual, expected string) bool {
   363  		for i := range matchers {
   364  			if !matchers[i](actual, expected) {
   365  				return false
   366  			}
   367  		}
   368  		return true
   369  	}
   370  	return func(c *Consequences) (state, string) {
   371  		if c.actions.lastError != nil {
   372  			return failed, fmt.Errorf("error: %w", c.actions.lastError).Error()
   373  		}
   374  		if !match(c.actions.lastOutput, message) {
   375  			return failed, fmt.Sprintf("output did not contain '%s'", message)
   376  		}
   377  		return succeeded, fmt.Sprintf("no error and output contained '%s'", message)
   378  	}
   379  }
   380  
   381  // Error asserts that the last command was an error with substring match
   382  func Error(message, err string, matchers ...func(string, string) bool) Expectation {
   383  	if len(matchers) == 0 {
   384  		matchers = append(matchers, strings.Contains)
   385  	}
   386  	match := func(actual, expected string) bool {
   387  		for i := range matchers {
   388  			if !matchers[i](actual, expected) {
   389  				return false
   390  			}
   391  		}
   392  		return true
   393  	}
   394  	return func(c *Consequences) (state, string) {
   395  		if c.actions.lastError == nil {
   396  			return failed, "no error"
   397  		}
   398  		if !match(c.actions.lastOutput, message) {
   399  			return failed, fmt.Sprintf("output does not contain '%s'", message)
   400  		}
   401  		if !match(c.actions.lastError.Error(), err) {
   402  			return failed, fmt.Sprintf("error does not contain '%s'", err)
   403  		}
   404  		return succeeded, fmt.Sprintf("error '%s'", message)
   405  	}
   406  }
   407  
   408  // ErrorRegex asserts that the last command was an error that matches given regex epxression
   409  func ErrorRegex(messagePattern, err string) Expectation {
   410  	return Error(messagePattern, err, func(actual, expected string) bool {
   411  		return regexp.MustCompile(expected).MatchString(actual)
   412  	})
   413  }
   414  
   415  // SuccessRegex asserts that the last command was successful and output matches given regex expression
   416  func SuccessRegex(messagePattern string) Expectation {
   417  	return Success(messagePattern, func(actual, expected string) bool {
   418  		return regexp.MustCompile(expected).MatchString(actual)
   419  	})
   420  }