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 }