feat: add configurable plan timeout to expire stale plans#6317
Open
demarsdouglas wants to merge 1 commit intorunatlantis:mainfrom
Open
feat: add configurable plan timeout to expire stale plans#6317demarsdouglas wants to merge 1 commit intorunatlantis:mainfrom
demarsdouglas wants to merge 1 commit intorunatlantis:mainfrom
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Adds an optional server-side “plan freshness” control to Atlantis by introducing a configurable plan timeout that prevents applying stale Terraform plans and automatically releases associated locks.
Changes:
- Introduces
--plan-timeout/ATLANTIS_PLAN_TIMEOUT(Gotime.Durationsyntax) and wires it through server config. - Implements apply-time plan expiration: delete the
.tfplanfile, release the project lock, and return a re-plan message when expired. - Adds unit tests for timeout parsing, lock time propagation, and apply behavior; updates server configuration docs.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
server/user_config.go |
Adds plan-timeout to UserConfig for Viper/mapstructure unmarshalling. |
cmd/server.go |
Registers --plan-timeout as a server flag (string) with help text and env var support. |
cmd/server_test.go |
Extends flag test coverage to include plan-timeout. |
server/server.go |
Adds ParsePlanTimeout, parses config at startup, and injects the duration into DefaultProjectCommandRunner. |
server/server_test.go |
Adds unit tests for ParsePlanTimeout. |
server/events/project_locker.go |
Exposes lock creation time via TryLockResponse.LockTime for downstream expiration checks. |
server/events/project_locker_test.go |
Adds test validating lock time propagation through the project locker wrapper. |
server/events/project_command_runner.go |
Enforces plan expiration during apply and performs cleanup (plan file + lock release). |
server/events/project_command_runner_test.go |
Adds tests for expired/not-expired/disabled timeout and zero lock-time behavior. |
runatlantis.io/docs/server-configuration.md |
Documents --plan-timeout usage and semantics. |
Comment on lines
+690
to
+701
| if p.PlanTimeout > 0 && !lockAttempt.LockTime.IsZero() && time.Since(lockAttempt.LockTime) > p.PlanTimeout { | ||
| ctx.Log.Info("plan has expired (created %s ago, timeout %s), discarding", time.Since(lockAttempt.LockTime).Round(time.Second), p.PlanTimeout) | ||
| planFile := filepath.Join(absPath, runtime.GetPlanFilename(ctx.Workspace, ctx.ProjectName)) | ||
| if removeErr := os.Remove(planFile); removeErr != nil && !os.IsNotExist(removeErr) { | ||
| ctx.Log.Err("failed to remove expired plan file: %v", removeErr) | ||
| } | ||
| if unlockErr := lockAttempt.UnlockFn(); unlockErr != nil { | ||
| ctx.Log.Err("failed to release lock for expired plan: %v", unlockErr) | ||
| } | ||
| return "", fmt.Sprintf("The plan has expired (created %s ago, timeout is %s). Please run `atlantis plan` again.", | ||
| time.Since(lockAttempt.LockTime).Round(time.Second), p.PlanTimeout), nil | ||
| } |
Comment on lines
+254
to
+258
| if tt.lockAcquired { | ||
| mockLocker.EXPECT().Unlock(lockKey).Return(nil, nil) | ||
| } else { | ||
| mockLocker.EXPECT().Unlock(lockKey).Return(nil, nil) | ||
| } |
| tests := []struct { | ||
| name string | ||
| lockAcquired bool | ||
| description string |
Signed-off-by: demarsdouglas <demarsdouglas@gmail.com>
a88288d to
5c792e6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
what
--plan-timeoutserver flag that sets a maximum age for Terraform plans. Whenatlantis applyis run after the timeout has elapsed, the plan is discarded, the lock is released, and the user sees a message asking them to re-plan.why
how
ProjectLockalready stores aTimefield when the lock is created duringatlantis plan.--plan-timeoutflag is parsed as atime.Duration(e.g.15m,2h30m).atlantis applyruns,doApply()checkstime.Since(lockTime)against the configured timeout..tfplanfile is removed, the lock is released, and a failure message is returned.tests
TestDefaultProjectCommandRunner_ApplyPlanExpired- expired plan is rejected, file deleted, lock releasedTestDefaultProjectCommandRunner_ApplyPlanNotExpired- recent plan proceeds normallyTestDefaultProjectCommandRunner_ApplyNoPlanTimeout- zero timeout preserves backward compatibilityTestDefaultProjectCommandRunner_ApplyPlanExpiredZeroLockTime- zero lock time skips the checkTestDefaultProjectLocker_TryLockPopulatesLockTime- lock time propagated for fresh and re-acquired locksTestParsePlanTimeout- valid durations, empty input, invalid strings, and negative durationsreferences
closes #270