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 }