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

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