github.com/argoproj/argo-cd/v2@v2.10.5/test/e2e/hook_test.go (about)

     1  package e2e
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/stretchr/testify/assert"
    10  	v1 "k8s.io/api/core/v1"
    11  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    12  
    13  	"github.com/argoproj/gitops-engine/pkg/health"
    14  	. "github.com/argoproj/gitops-engine/pkg/sync/common"
    15  
    16  	. "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    17  	. "github.com/argoproj/argo-cd/v2/test/e2e/fixture"
    18  	. "github.com/argoproj/argo-cd/v2/test/e2e/fixture/app"
    19  	. "github.com/argoproj/argo-cd/v2/util/errors"
    20  )
    21  
    22  func TestPreSyncHookSuccessful(t *testing.T) {
    23  	// special-case that the pod remains in the running state, but we don't really care, because this is only used for
    24  	// determining overall operation status is a sync with >1 wave/phase
    25  	testHookSuccessful(t, HookTypePreSync)
    26  }
    27  
    28  func TestSyncHookSuccessful(t *testing.T) {
    29  	testHookSuccessful(t, HookTypeSync)
    30  }
    31  
    32  func TestPostSyncHookSuccessful(t *testing.T) {
    33  	testHookSuccessful(t, HookTypePostSync)
    34  }
    35  
    36  // make sure we can run a standard sync hook
    37  func testHookSuccessful(t *testing.T, hookType HookType) {
    38  	Given(t).
    39  		Path("hook").
    40  		When().
    41  		PatchFile("hook.yaml", fmt.Sprintf(`[{"op": "replace", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/hook": "%s"}}]`, hookType)).
    42  		CreateApp().
    43  		Sync().
    44  		Then().
    45  		Expect(OperationPhaseIs(OperationSucceeded)).
    46  		Expect(SyncStatusIs(SyncStatusCodeSynced)).
    47  		Expect(ResourceSyncStatusIs("Pod", "pod", SyncStatusCodeSynced)).
    48  		Expect(ResourceHealthIs("Pod", "pod", health.HealthStatusHealthy)).
    49  		Expect(ResourceResultNumbering(2)).
    50  		Expect(ResourceResultIs(ResourceResult{Version: "v1", Kind: "Pod", Namespace: DeploymentNamespace(), Name: "hook", Message: "pod/hook created", HookType: hookType, HookPhase: OperationSucceeded, SyncPhase: SyncPhase(hookType)}))
    51  }
    52  
    53  func TestPostDeleteHook(t *testing.T) {
    54  	Given(t).
    55  		Path("post-delete-hook").
    56  		When().
    57  		CreateApp().
    58  		Refresh(RefreshTypeNormal).
    59  		Delete(true).
    60  		Then().
    61  		Expect(DoesNotExist()).
    62  		AndAction(func() {
    63  			hooks, err := KubeClientset.CoreV1().Pods(DeploymentNamespace()).List(context.Background(), metav1.ListOptions{})
    64  			CheckError(err)
    65  			assert.Len(t, hooks.Items, 1)
    66  			assert.Equal(t, "hook", hooks.Items[0].Name)
    67  		})
    68  
    69  }
    70  
    71  // make sure that that hooks do not appear in "argocd app diff"
    72  func TestHookDiff(t *testing.T) {
    73  	Given(t).
    74  		Path("hook").
    75  		When().
    76  		CreateApp().
    77  		Then().
    78  		And(func(_ *Application) {
    79  			output, err := RunCli("app", "diff", Name())
    80  			assert.Error(t, err)
    81  			assert.Contains(t, output, "name: pod")
    82  			assert.NotContains(t, output, "name: hook")
    83  		})
    84  }
    85  
    86  // make sure that if pre-sync fails, we fail the app and we do not create the pod
    87  func TestPreSyncHookFailure(t *testing.T) {
    88  	Given(t).
    89  		Path("hook").
    90  		When().
    91  		PatchFile("hook.yaml", `[{"op": "replace", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/hook": "PreSync"}}]`).
    92  		// make hook fail
    93  		PatchFile("hook.yaml", `[{"op": "replace", "path": "/spec/containers/0/command", "value": ["false"]}]`).
    94  		CreateApp().
    95  		IgnoreErrors().
    96  		Sync().
    97  		Then().
    98  		Expect(Error("hook  Failed              PreSync", "")).
    99  		// make sure resource are also printed
   100  		Expect(Error("pod   OutOfSync  Missing", "")).
   101  		Expect(OperationPhaseIs(OperationFailed)).
   102  		// if a pre-sync hook fails, we should not start the main sync
   103  		Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
   104  		Expect(ResourceResultNumbering(1)).
   105  		Expect(ResourceSyncStatusIs("Pod", "pod", SyncStatusCodeOutOfSync))
   106  }
   107  
   108  // make sure that if sync fails, we fail the app and we did create the pod
   109  func TestSyncHookFailure(t *testing.T) {
   110  	Given(t).
   111  		Path("hook").
   112  		When().
   113  		// make hook fail
   114  		PatchFile("hook.yaml", `[{"op": "replace", "path": "/spec/containers/0/command/0", "value": "false"}]`).
   115  		CreateApp().
   116  		IgnoreErrors().
   117  		Sync().
   118  		Then().
   119  		Expect(OperationPhaseIs(OperationFailed)).
   120  		// even thought the hook failed, we expect the pod to be in sync
   121  		Expect(SyncStatusIs(SyncStatusCodeSynced)).
   122  		Expect(ResourceResultNumbering(2)).
   123  		Expect(ResourceSyncStatusIs("Pod", "pod", SyncStatusCodeSynced))
   124  }
   125  
   126  // make sure that if the deployments fails, we still get success and synced
   127  func TestSyncHookResourceFailure(t *testing.T) {
   128  	Given(t).
   129  		Path("hook-and-deployment").
   130  		When().
   131  		CreateApp().
   132  		Sync().
   133  		Then().
   134  		Expect(OperationPhaseIs(OperationSucceeded)).
   135  		Expect(SyncStatusIs(SyncStatusCodeSynced)).
   136  		Expect(HealthIs(health.HealthStatusProgressing))
   137  }
   138  
   139  // make sure that if post-sync fails, we fail the app and we did not create the pod
   140  func TestPostSyncHookFailure(t *testing.T) {
   141  	Given(t).
   142  		Path("hook").
   143  		When().
   144  		PatchFile("hook.yaml", `[{"op": "replace", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/hook": "PostSync"}}]`).
   145  		// make hook fail
   146  		PatchFile("hook.yaml", `[{"op": "replace", "path": "/spec/containers/0/command/0", "value": "false"}]`).
   147  		CreateApp().
   148  		IgnoreErrors().
   149  		Sync().
   150  		Then().
   151  		Expect(OperationPhaseIs(OperationFailed)).
   152  		Expect(SyncStatusIs(SyncStatusCodeSynced)).
   153  		Expect(ResourceResultNumbering(2)).
   154  		Expect(ResourceSyncStatusIs("Pod", "pod", SyncStatusCodeSynced))
   155  }
   156  
   157  // make sure that if the pod fails, we do not run the post-sync hook
   158  func TestPostSyncHookPodFailure(t *testing.T) {
   159  	Given(t).
   160  		Path("hook").
   161  		When().
   162  		IgnoreErrors().
   163  		PatchFile("hook.yaml", `[{"op": "add", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/hook": "PostSync"}}]`).
   164  		// make pod fail
   165  		PatchFile("pod.yaml", `[{"op": "replace", "path": "/spec/containers/0/command/0", "value": "false"}]`).
   166  		CreateApp().
   167  		Sync().
   168  		Then().
   169  		// TODO - I feel like this should be a failure, not success
   170  		Expect(SyncStatusIs(SyncStatusCodeSynced)).
   171  		Expect(ResourceSyncStatusIs("Pod", "pod", SyncStatusCodeSynced)).
   172  		Expect(ResourceHealthIs("Pod", "pod", health.HealthStatusDegraded)).
   173  		Expect(ResourceResultNumbering(1)).
   174  		Expect(NotPod(func(p v1.Pod) bool { return p.Name == "hook" }))
   175  }
   176  
   177  func TestSyncFailHookPodFailure(t *testing.T) {
   178  	// Tests that a SyncFail hook will successfully run upon a pod failure (which leads to a sync failure)
   179  	Given(t).
   180  		Path("hook").
   181  		When().
   182  		IgnoreErrors().
   183  		AddFile("sync-fail-hook.yaml", `
   184  apiVersion: v1
   185  kind: Pod
   186  metadata:
   187    annotations:
   188      argocd.argoproj.io/hook: SyncFail
   189    name: sync-fail-hook
   190  spec:
   191    containers:
   192      - command:
   193          - "true"
   194        image: "quay.io/argoprojlabs/argocd-e2e-container:0.1"
   195        imagePullPolicy: IfNotPresent
   196        name: main
   197    restartPolicy: Never
   198  `).
   199  		PatchFile("hook.yaml", `[{"op": "replace", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/hook": "PostSync"}}]`).
   200  		PatchFile("hook.yaml", `[{"op": "replace", "path": "/spec/containers/0/command/0", "value": "false"}]`).
   201  		CreateApp().
   202  		Sync().
   203  		Then().
   204  		Expect(ResourceResultIs(ResourceResult{Version: "v1", Kind: "Pod", Namespace: DeploymentNamespace(), Name: "sync-fail-hook", Message: "pod/sync-fail-hook created", HookType: HookTypeSyncFail, HookPhase: OperationSucceeded, SyncPhase: SyncPhaseSyncFail})).
   205  		Expect(OperationPhaseIs(OperationFailed))
   206  }
   207  
   208  func TestSyncFailHookPodFailureSyncFailFailure(t *testing.T) {
   209  	// Tests that a failing SyncFail hook will successfully be marked as failed
   210  	Given(t).
   211  		Path("hook").
   212  		When().
   213  		IgnoreErrors().
   214  		AddFile("successful-sync-fail-hook.yaml", `
   215  apiVersion: v1
   216  kind: Pod
   217  metadata:
   218    annotations:
   219      argocd.argoproj.io/hook: SyncFail
   220    name: successful-sync-fail-hook
   221  spec:
   222    containers:
   223      - command:
   224          - "true"
   225        image: "quay.io/argoprojlabs/argocd-e2e-container:0.1"
   226        imagePullPolicy: IfNotPresent
   227        name: main
   228    restartPolicy: Never
   229  `).
   230  		AddFile("failed-sync-fail-hook.yaml", `
   231  apiVersion: v1
   232  kind: Pod
   233  metadata:
   234    annotations:
   235      argocd.argoproj.io/hook: SyncFail
   236    name: failed-sync-fail-hook
   237  spec:
   238    containers:
   239      - command:
   240          - "false"
   241        image: "quay.io/argoprojlabs/argocd-e2e-container:0.1"
   242        imagePullPolicy: IfNotPresent
   243        name: main
   244    restartPolicy: Never
   245  `).
   246  		PatchFile("hook.yaml", `[{"op": "replace", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/hook": "PostSync"}}]`).
   247  		PatchFile("hook.yaml", `[{"op": "replace", "path": "/spec/containers/0/command/0", "value": "false"}]`).
   248  		CreateApp().
   249  		Sync().
   250  		Then().
   251  		Expect(ResourceResultIs(ResourceResult{Version: "v1", Kind: "Pod", Namespace: DeploymentNamespace(), Name: "successful-sync-fail-hook", Message: "pod/successful-sync-fail-hook created", HookType: HookTypeSyncFail, HookPhase: OperationSucceeded, SyncPhase: SyncPhaseSyncFail})).
   252  		Expect(ResourceResultIs(ResourceResult{Version: "v1", Kind: "Pod", Namespace: DeploymentNamespace(), Name: "failed-sync-fail-hook", Message: `container "main" failed with exit code 1`, HookType: HookTypeSyncFail, HookPhase: OperationFailed, SyncPhase: SyncPhaseSyncFail})).
   253  		Expect(OperationPhaseIs(OperationFailed))
   254  }
   255  
   256  // make sure that we delete the hook on success
   257  func TestHookDeletePolicyHookSucceededHookExit0(t *testing.T) {
   258  	Given(t).
   259  		Path("hook").
   260  		When().
   261  		PatchFile("hook.yaml", `[{"op": "add", "path": "/metadata/annotations/argocd.argoproj.io~1hook-delete-policy", "value": "HookSucceeded"}]`).
   262  		CreateApp().
   263  		Sync().
   264  		Then().
   265  		Expect(OperationPhaseIs(OperationSucceeded)).
   266  		Expect(NotPod(func(p v1.Pod) bool { return p.Name == "hook" }))
   267  }
   268  
   269  // make sure that we delete the hook on failure, if policy is set
   270  func TestHookDeletePolicyHookSucceededHookExit1(t *testing.T) {
   271  	Given(t).
   272  		Path("hook").
   273  		When().
   274  		PatchFile("hook.yaml", `[{"op": "add", "path": "/metadata/annotations/argocd.argoproj.io~1hook-delete-policy", "value": "HookSucceeded"}]`).
   275  		PatchFile("hook.yaml", `[{"op": "replace", "path": "/spec/containers/0/command/0", "value": "false"}]`).
   276  		CreateApp().
   277  		IgnoreErrors().
   278  		Sync().
   279  		Then().
   280  		Expect(OperationPhaseIs(OperationFailed)).
   281  		Expect(ResourceResultNumbering(2)).
   282  		Expect(Pod(func(p v1.Pod) bool { return p.Name == "hook" }))
   283  }
   284  
   285  // make sure that we do NOT delete the hook on success if failure policy is set
   286  func TestHookDeletePolicyHookFailedHookExit0(t *testing.T) {
   287  	Given(t).
   288  		Path("hook").
   289  		When().
   290  		PatchFile("hook.yaml", `[{"op": "add", "path": "/metadata/annotations/argocd.argoproj.io~1hook-delete-policy", "value": "HookFailed"}]`).
   291  		CreateApp().
   292  		Sync().
   293  		Then().
   294  		Expect(OperationPhaseIs(OperationSucceeded)).
   295  		Expect(ResourceResultNumbering(2)).
   296  		Expect(Pod(func(p v1.Pod) bool { return p.Name == "hook" }))
   297  }
   298  
   299  // make sure that we do delete the hook on failure if failure policy is set
   300  func TestHookDeletePolicyHookFailedHookExit1(t *testing.T) {
   301  	Given(t).
   302  		Path("hook").
   303  		When().
   304  		IgnoreErrors().
   305  		PatchFile("hook.yaml", `[{"op": "add", "path": "/metadata/annotations/argocd.argoproj.io~1hook-delete-policy", "value": "HookFailed"}]`).
   306  		PatchFile("hook.yaml", `[{"op": "replace", "path": "/spec/containers/0/command/0", "value": "false"}]`).
   307  		CreateApp().
   308  		Sync().
   309  		Then().
   310  		Expect(OperationPhaseIs(OperationFailed)).
   311  		Expect(ResourceResultNumbering(2)).
   312  		Expect(NotPod(func(p v1.Pod) bool { return p.Name == "hook" }))
   313  }
   314  
   315  // make sure that we can run the hook twice
   316  func TestHookBeforeHookCreation(t *testing.T) {
   317  	var creationTimestamp1 string
   318  	Given(t).
   319  		Path("hook").
   320  		When().
   321  		PatchFile("hook.yaml", `[{"op": "add", "path": "/metadata/annotations/argocd.argoproj.io~1hook-delete-policy", "value": "BeforeHookCreation"}]`).
   322  		CreateApp().
   323  		Sync().
   324  		Then().
   325  		Expect(OperationPhaseIs(OperationSucceeded)).
   326  		Expect(SyncStatusIs(SyncStatusCodeSynced)).
   327  		Expect(HealthIs(health.HealthStatusHealthy)).
   328  		Expect(ResourceResultNumbering(2)).
   329  		// the app will be in health+n-sync before this hook has run
   330  		Expect(Pod(func(p v1.Pod) bool { return p.Name == "hook" })).
   331  		And(func(_ *Application) {
   332  			var err error
   333  			creationTimestamp1, err = getCreationTimestamp()
   334  			CheckError(err)
   335  			assert.NotEmpty(t, creationTimestamp1)
   336  			// pause to ensure that timestamp will change
   337  			time.Sleep(2 * time.Second)
   338  		}).
   339  		When().
   340  		Sync().
   341  		Then().
   342  		Expect(OperationPhaseIs(OperationSucceeded)).
   343  		Expect(SyncStatusIs(SyncStatusCodeSynced)).
   344  		Expect(HealthIs(health.HealthStatusHealthy)).
   345  		Expect(ResourceResultNumbering(2)).
   346  		Expect(Pod(func(p v1.Pod) bool { return p.Name == "hook" })).
   347  		And(func(_ *Application) {
   348  			creationTimestamp2, err := getCreationTimestamp()
   349  			CheckError(err)
   350  			assert.NotEmpty(t, creationTimestamp2)
   351  			assert.NotEqual(t, creationTimestamp1, creationTimestamp2)
   352  		})
   353  }
   354  
   355  // edge-case where we are unable to delete the hook because it is still running
   356  func TestHookBeforeHookCreationFailure(t *testing.T) {
   357  	Given(t).
   358  		Timeout(1).
   359  		Path("hook").
   360  		When().
   361  		PatchFile("hook.yaml", `[
   362  	{"op": "add", "path": "/metadata/annotations/argocd.argoproj.io~1hook-delete-policy", "value": "BeforeHookCreation"},
   363  	{"op": "replace", "path": "/spec/containers/0/command", "value": ["sleep", "3"]}
   364  ]`).
   365  		CreateApp().
   366  		IgnoreErrors().
   367  		Sync().
   368  		DoNotIgnoreErrors().
   369  		TerminateOp().
   370  		Then().
   371  		Expect(OperationPhaseIs(OperationFailed)).
   372  		Expect(ResourceResultNumbering(2))
   373  }
   374  
   375  func getCreationTimestamp() (string, error) {
   376  	return Run(".", "kubectl", "-n", DeploymentNamespace(), "get", "pod", "hook", "-o", "jsonpath={.metadata.creationTimestamp}")
   377  }
   378  
   379  // make sure that we never create something annotated with Skip
   380  func TestHookSkip(t *testing.T) {
   381  	Given(t).
   382  		Path("hook").
   383  		When().
   384  		// should not create this pod
   385  		PatchFile("pod.yaml", `[{"op": "replace", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/hook": "Skip"}}]`).
   386  		CreateApp().
   387  		Sync().
   388  		Then().
   389  		Expect(OperationPhaseIs(OperationSucceeded)).
   390  		Expect(ResourceResultNumbering(1)).
   391  		Expect(NotPod(func(p v1.Pod) bool { return p.Name == "pod" }))
   392  }
   393  
   394  // make sure that we do NOT name non-hook resources in they are unnamed
   395  func TestNamingNonHookResource(t *testing.T) {
   396  	Given(t).
   397  		Async(true).
   398  		Path("hook").
   399  		When().
   400  		PatchFile("pod.yaml", `[{"op": "remove", "path": "/metadata/name"}]`).
   401  		CreateApp().
   402  		Sync().
   403  		Then().
   404  		Expect(OperationPhaseIs(OperationFailed))
   405  }
   406  
   407  // make sure that we name hook resources in they are unnamed
   408  func TestAutomaticallyNamingUnnamedHook(t *testing.T) {
   409  	Given(t).
   410  		Async(true).
   411  		Path("hook").
   412  		When().
   413  		PatchFile("hook.yaml", `[{"op": "remove", "path": "/metadata/name"}]`).
   414  		// make this part of two sync tasks
   415  		PatchFile("hook.yaml", `[{"op": "replace", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/hook": "PreSync,PostSync"}}]`).
   416  		CreateApp().
   417  		Sync().
   418  		Then().
   419  		Expect(OperationPhaseIs(OperationSucceeded)).
   420  		Expect(SyncStatusIs(SyncStatusCodeSynced)).
   421  		And(func(app *Application) {
   422  			resources := app.Status.OperationState.SyncResult.Resources
   423  			assert.Equal(t, 3, len(resources))
   424  			// make sure we don't use the same name
   425  			assert.Contains(t, resources[0].Name, "presync")
   426  			assert.Contains(t, resources[2].Name, "postsync")
   427  		})
   428  }