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