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 }