diff --git a/export_test.go b/export_test.go index e415a5a7..7a56b1bf 100644 --- a/export_test.go +++ b/export_test.go @@ -40,6 +40,7 @@ var ( LifecycleStageIndex = lifecycleStageIndex ValidateLifecycleStageSupported = validateLifecycleStageSupported EvaluateDeploymentStatus = evaluateDeploymentStatus + PausedHookIDs = pausedHookIDs ) type WaitDeploymentResult = waitDeploymentResult diff --git a/wait.go b/wait.go index 1911a124..460e82c5 100644 --- a/wait.go +++ b/wait.go @@ -460,17 +460,27 @@ func evaluateDeploymentStatus(dp *types.ServiceDeployment, done func(*types.Serv // deploymentPaused reports whether a pause lifecycle hook of the deployment is // awaiting action. func (d *App) deploymentPaused(dp *types.ServiceDeployment) bool { + ids := pausedHookIDs(dp) + for _, id := range ids { + d.LogInfo("deployment paused at lifecycle hook", + "hook_id", id, + "lifecycle_stage", string(dp.LifecycleStage), + ) + } + return len(ids) > 0 +} + +// pausedHookIDs returns the IDs of pause lifecycle hooks of the deployment +// that are awaiting action. +func pausedHookIDs(dp *types.ServiceDeployment) []string { + var ids []string for _, hook := range dp.LifecycleHookDetails { if hook.TargetType == types.DeploymentLifecycleHookTargetTypePause && hook.Status == types.DeploymentLifecycleHookStatusAwaitingAction { - d.LogInfo("deployment paused at lifecycle hook", - "hook_id", aws.ToString(hook.HookId), - "lifecycle_stage", string(dp.LifecycleStage), - ) - return true + ids = append(ids, aws.ToString(hook.HookId)) } } - return false + return ids } // WaitServiceDeployLifecycleStage returns a waitFunc that waits until the @@ -492,6 +502,7 @@ func (d *App) WaitServiceDeployLifecycleStage(stage types.ServiceDeploymentLifec d.LogWarn("deployment strategy is not set; a ROLLING deployment reports no lifecycle stage, so this waits until the deployment completes") } d.LogInfo("Waiting for service deployment lifecycle stage...", "stage", string(stage)) + notified := map[string]struct{}{} // Stages such as PRODUCTION_TRAFFIC_SHIFT are transient and can be // skipped between polls, so compare positions rather than equality. return d.waitServiceDeployment(ctx, knownDeploymentArn, func(dp *types.ServiceDeployment) bool { @@ -499,6 +510,19 @@ func (d *App) WaitServiceDeployLifecycleStage(stage types.ServiceDeploymentLifec d.LogInfo("service deployment reached the lifecycle stage", "stage", string(dp.LifecycleStage)) return true } + // A pause lifecycle hook awaiting action blocks the deployment, so + // the target stage never arrives until the deployment is continued. + for _, id := range pausedHookIDs(dp) { + if _, ok := notified[id]; ok { + continue + } + notified[id] = struct{}{} + d.LogWarn("deployment is paused at a lifecycle hook; the target lifecycle stage will not be reached until the deployment is continued (run `ecspresso continue`)", + "hook_id", id, + "lifecycle_stage", string(dp.LifecycleStage), + "target_stage", string(stage), + ) + } return false }) } diff --git a/wait_test.go b/wait_test.go index 8503e576..d308fe49 100644 --- a/wait_test.go +++ b/wait_test.go @@ -6,9 +6,40 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ecs/types" + "github.com/google/go-cmp/cmp" "github.com/kayac/ecspresso/v2" ) +func TestPausedHookIDs(t *testing.T) { + dp := &types.ServiceDeployment{ + LifecycleHookDetails: []types.DeploymentLifecycleHookDetail{ + { + HookId: aws.String("hook-awaiting"), + TargetType: types.DeploymentLifecycleHookTargetTypePause, + Status: types.DeploymentLifecycleHookStatusAwaitingAction, + }, + { + HookId: aws.String("hook-in-progress"), + TargetType: types.DeploymentLifecycleHookTargetTypePause, + Status: types.DeploymentLifecycleHookStatusInProgress, + }, + { + HookId: aws.String("hook-lambda"), + TargetType: types.DeploymentLifecycleHookTargetTypeAwsLambda, + Status: types.DeploymentLifecycleHookStatusAwaitingAction, + }, + }, + } + want := []string{"hook-awaiting"} + if diff := cmp.Diff(want, ecspresso.PausedHookIDs(dp)); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + + if got := ecspresso.PausedHookIDs(&types.ServiceDeployment{}); got != nil { + t.Errorf("expected nil for no hooks, got %v", got) + } +} + func TestLifecycleStageIndex(t *testing.T) { // The waiter compares positions instead of equality, so pin the full // ordering of the stages within the deployment lifecycle.