diff --git a/README.md b/README.md index ec93dd36..10e79775 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,11 @@ If you add/remove workspaces outside of Pug, you can instruct Pug to reload work A run represents a terraform plan and the optional apply of that plan. Under the hood, it invokes `terraform plan -out `. Should you then apply the run, it invokes `terraform apply `. -A run starts in the `pending` state. If its workspace doesn't have a current run, then Pug transitions it into the `scheduled` state and sets it as the workspace's current run. Otherwise the run remains in the `pending` state until the current run has finished. +A run starts in the `pending` state. It remains in that state until it can be scheduled. It can only be scheduled once all runs created prior to it on the same workspace have finished. + +An exception to this rule is if the previous run is in the `planned` state. In which case the previous run is placed into the `stale` termination state, i.e. its plan file is deemed stale and cannot be applied. + +Once a run is scheduled, it's placed into `scheduled` state and it's designated as the *current run* for the workspace. If there are no blocked tasks running on its workspace and module (see tasks below) then the run transitions into the `plan queued` state. Once there is sufficient task capacity, the run enters the `planning` state, and `terraform plan` is invoked. diff --git a/internal/app/helpers_test.go b/internal/app/helpers_test.go index aa701b2c..0daa6911 100644 --- a/internal/app/helpers_test.go +++ b/internal/app/helpers_test.go @@ -114,7 +114,7 @@ func cleanupArtefacts(workdir string, opts setupOptions) { } -// setupProviderMirror configures a dedicated provider filesystem mirror for for +// setupProviderMirror configures a dedicated provider filesystem mirror for // a test. func setupProviderMirror(t *testing.T) { t.Helper() diff --git a/internal/app/run_test.go b/internal/app/run_test.go index 252dc2aa..eba079db 100644 --- a/internal/app/run_test.go +++ b/internal/app/run_test.go @@ -14,6 +14,71 @@ func TestRun(t *testing.T) { initAndApplyModuleA(t, tm) } +// TestRun_Stale tests that a planned run is placed into the 'stale' state when +// a succeeding run is created. +func TestRun_Stale(t *testing.T) { + tm := setup(t, "./testdata/module_list") + + // Wait for module to be loaded + waitFor(t, tm, func(s string) bool { + return strings.Contains(s, "modules/a") + }) + + // Initialize module + tm.Type("i") + waitFor(t, tm, func(s string) bool { + return strings.Contains(s, "Terraform has been successfully initialized!") + }) + + // Go to workspaces + tm.Type("W") + + // Wait for workspace to be loaded + waitFor(t, tm, func(s string) bool { + return strings.Contains(s, "default") + }) + + // Create plan for first workspace + tm.Type("p") + + // User should now be taken to the run page... + + // Expect to see summary of changes + waitFor(t, tm, func(s string) bool { + // Remove bold formatting + s = internal.StripAnsi(s) + return strings.Contains(s, "Plan: 10 to add, 0 to change, 0 to destroy.") + }) + + // Go to its workspace page + tm.Type("w") + + // Expect one run in the planned state + waitFor(t, tm, func(s string) bool { + return strings.Contains(s, "runs (1)") && matchPattern(t, `planned.*\+10~0\-0`, s) + }) + + // Start another run + tm.Type("p") + + // Expect to see summary of changes, again. + waitFor(t, tm, func(s string) bool { + // Remove bold formatting + s = internal.StripAnsi(s) + return strings.Contains(s, "Plan: 10 to add, 0 to change, 0 to destroy.") + }) + + // Go to its workspace page + tm.Type("w") + + // Expect two runs, one in the planned state, one in the stale state + waitFor(t, tm, func(s string) bool { + return strings.Contains(s, "runs (2)") && + matchPattern(t, `planned.*\+10~0\-0`, s) && + matchPattern(t, `stale.*\+10~0\-0`, s) + }) +} + func TestRun_WithVars(t *testing.T) { tm := setup(t, "./testdata/run_with_vars") diff --git a/internal/run/run.go b/internal/run/run.go index 93a7f892..2f38aea2 100644 --- a/internal/run/run.go +++ b/internal/run/run.go @@ -25,6 +25,7 @@ const ( ApplyQueued Status = "apply queued" Applying Status = "applying" Applied Status = "applied" + Stale Status = "stale" Errored Status = "errored" Canceled Status = "canceled" Discarded Status = "discarded" @@ -126,7 +127,7 @@ func (r *Run) PlanArgs() []string { func (r *Run) IsFinished() bool { switch r.Status { - case NoChanges, Applied, Errored, Canceled: + case NoChanges, Applied, Errored, Canceled, Stale: return true default: return false diff --git a/internal/run/scheduler.go b/internal/run/scheduler.go index 34638bc0..9c83de24 100644 --- a/internal/run/scheduler.go +++ b/internal/run/scheduler.go @@ -27,8 +27,20 @@ func StartScheduler(runs *Service, workspaces *workspace.Service) { for _, run := range s.schedule() { // Update status from pending to scheduled run.updateStatus(Scheduled) + + // If the current current run is in planned state, mark it as stale + // + // TODO: handle errors + ws, _ := workspaces.Get(run.WorkspaceID()) + if runID := ws.CurrentRunID; runID != nil { + current, _ := runs.Get(*runID) + if current.Status == Planned { + current.updateStatus(Stale) + } + } + // Set run as workspace's current run - workspaces.SetCurrentRun(run.WorkspaceID(), run.ID) + _ = workspaces.SetCurrentRun(run.WorkspaceID(), run.ID) // Trigger a plan task _, _ = runs.plan(run) } diff --git a/internal/tui/helpers.go b/internal/tui/helpers.go index 36e288a1..9fcdc12f 100644 --- a/internal/tui/helpers.go +++ b/internal/tui/helpers.go @@ -162,13 +162,15 @@ func (h *Helpers) RunStatus(r *run.Run) string { color = Green case run.Errored: color = Red + case run.Stale: + color = Orange } return Regular.Copy().Foreground(color).Render(string(r.Status)) } func (h *Helpers) LatestRunReport(r *run.Run) string { switch r.Status { - case run.Planned, run.NoChanges: + case run.Planned, run.NoChanges, run.Stale: return h.RunReport(r.PlanReport) case run.Applied: return h.RunReport(r.ApplyReport)