terraform/internal/moduletest/graph/wait.go
Liam Cervante 551ba2e525
Implement controlling destroy functionality within Terraform Test (#37359)
* Add ability to parse backend blocks present in a test file's run blocks, validate configuration (#36541)

* Add ability to parse backend blocks from a run block

* Add validation to avoid multiple backend blocks across run blocks that use the same internal state file. Update tests.

* Add validation to avoid multiple backend blocks within a single run block. Update tests.

* Remove use of quotes in diagnostic messages

* Add validation to avoid backend blocks being used in plan run blocks. Update tests.

* Correct local backend blocks in new test fixtures

* Add test to show that different test files can use same backend block for same state key.

* Add validation to enforce state-storage backend types are used

* Remove TODO comment

We only need to consider one file at a time when checking if a state_key already has a backend associated with it; parallelism in `terraform test` is scoped down to individual files.

* Add validation to assert that the backend block must be in the first apply command for an internal state

* Consolidate backend block validation inside a single if statement

* Add initial version of validation that ensures a backend isn't re-used within a file

* Explicitly set the state_key at the point of parsing the config

TODO: What should be done with method (moduletest.Run).GetStateKey?

* Update test fixture now that reusing backend configs has been made invalid

* Add automated test showing validation of reused configuration blocks

* Skip test due to flakiness, minor change to test config naming

* Update test so it tolerates non-deterministic order run blocks are evaluated in

* Remove unnecessary value assignment to r.StateKey

* Replace use of GetStateKey() with accessing the state key that's now set during test config parsing

* Fix bug so that run blocks using child modules get the correct state key set at parsing time

* Update acceptance test to also cover scenario where root and child module state keys are in use

* Update test name

* Add newline to regex

* Ensure consistent place where repeat backend error is raised from

* Write leftover test state(s) to file (#36614)

* Add additional validation that the backend used in a run is a supported type (#36648)

* Prevent test run when leftover state data is present (#36685)

* `test`: Set the initial state for a state files from a backend, allow the run that defines a backend to write state to the backend (#36646)

* Allow use of backend block to set initial state for a state key

* Note about alternative place to keep 'backend factories'

* Allow the run block defining the backend to write state to it

* Fix rebase

* Change to accessing backend init functions via ContextOpts

* Add tests demonstrating how runs containing backend blocks use and update persisted state

* Fix test fixture

* Address test failure due to trouble opening the state file

This problem doesn't happen on MacOS, so I assume is due to the Linux environment of GitHub runners.

* Fix issue with paths properly

I hope

* Fix defect in test assertion

* Pivot back to approach introduced in 4afc3d7

* Let failing tests write to persistent state, add test case covering that.

I split the acceptance tests into happy/unhappy paths for this, which required some of the helper functions' declarations to be raised up to package-level.

* Change how we update internal state files, so that information about the associated backend is never lost

* Fix UpdateStateFile

* Ensure that the states map set by TestStateTransformer associates a backend with the correct run.

* Misc spelling fixes in comments and a log

* Replace state get/set functions with existing helpers (#36747)

* Replace state get/set functions with existing helpers

* Compare to string representation of state

* Compare to string representation of state

* Terraform Test: Allow skipping cleanup of entire test file or individual run blocks (#36729)

* Add validation to enforce skip_cleanup=false cannot be used with backend blocks (#36857)

* Integrate use of backend blocks in tests with skip_cleanup feature (#36848)

* Fix nil pointer error, update test to not be table-driven

* Make using a backend block implicitly set skip_cleanup to true

* Stop state artefacts being created when a backend is in use and no cleanup errors have occurred

* Return diagnostics so calling code knows if cleanup experienced issues or not

* Update tests to show that when cleanup fails a state artefact is created

* Add comment about why diag not returned

* Bug fix - actually pull in the state from the state manager!

* Split and simplify (?) tests to show the backend block can create and/or reuse prior state

* Update test to use new fixtures, assert about state artefact. Fix nil pointer

* Update test fixture in use, add guardrail for flakiness of forced error during cleanup

* Refactor so resource ID set in only one place

* Add documentation for using a `backend` block during `test` (#36832)

* Add backend as a documented block in a run block

* Add documentation about backend blocks in run blocks.

* Make the relationship between backends and state keys more clear, other improvements

* More test documentation (#36838)

* Terraform Test: cleanup command (#36847)

* Allow cleanup of states that depend on prior runs outputs (#36902)

* terraform test: refactor graph edge calculation

* create fake run block nodes during cleanup operation

* tidy up TODOs

* fix tests

* remove old changes

* Update internal/moduletest/graph/node_state_cleanup.go

Co-authored-by: Samsondeen <40821565+dsa0x@users.noreply.github.com>

* Improve diagnostics around skip_cleanup conflicts (#37385)

* Improve diagnostics around skip_cleanup conflicts

* remove unused dynamic node

* terraform test: refactor manifest file for simplicity (#37412)

* test: refactor apply and plan functions so no run block is needed

* terraform test: write and load state manifest files

* Terraform Test: Allow skipping cleanup of entire test file or individual run blocks (#36729)

* terraform test: add support for skip_cleanup attr

* terraform test: add cleanup command

* terraform test: add backend blocks

* pause

* fix tests

* remove commented code

* terraform test: make controlling destroy functionality experimental (#37419)

* address comments

* Update internal/moduletest/graph/node_state_cleanup.go

Co-authored-by: Samsondeen <40821565+dsa0x@users.noreply.github.com>

---------

Co-authored-by: Samsondeen <40821565+dsa0x@users.noreply.github.com>

* add experimental changelog entries

---------

Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com>
Co-authored-by: Samsondeen <40821565+dsa0x@users.noreply.github.com>
Co-authored-by: Samsondeen Dare <samsondeen.dare@hashicorp.com>
2025-09-10 17:22:20 +02:00

163 lines
4.2 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package graph
import (
"context"
"fmt"
"log"
"sync/atomic"
"time"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terraform"
)
// operationWaiter waits for an operation within
// a test run execution to complete.
type operationWaiter struct {
ctx *terraform.Context
runningCtx context.Context
run *moduletest.Run
file *moduletest.File
created []*plans.ResourceInstanceChangeSrc
progress atomicProgress[moduletest.Progress]
start int64
identifier string
finished bool
evalCtx *EvalContext
renderer views.Test
}
type atomicProgress[T moduletest.Progress] struct {
internal atomic.Value
}
func (a *atomicProgress[T]) Load() T {
return a.internal.Load().(T)
}
func (a *atomicProgress[T]) Store(progress T) {
a.internal.Store(progress)
}
// NewOperationWaiter creates a new operation waiter.
func NewOperationWaiter(ctx *terraform.Context, evalCtx *EvalContext, file *moduletest.File, run *moduletest.Run,
progress moduletest.Progress, start int64) *operationWaiter {
identifier := "validate"
if file != nil {
identifier = file.Name
if run != nil {
identifier = fmt.Sprintf("%s/%s", identifier, run.Name)
}
}
p := atomicProgress[moduletest.Progress]{}
p.Store(progress)
return &operationWaiter{
ctx: ctx,
run: run,
file: file,
progress: p,
start: start,
identifier: identifier,
evalCtx: evalCtx,
renderer: evalCtx.Renderer(),
}
}
// Run executes the given function in a goroutine and waits for it to finish.
// If the function finishes successfully, it returns false. If the function is cancelled or
// interrupted, it returns true.
func (w *operationWaiter) Run(fn func()) bool {
runningCtx, doneRunning := context.WithCancel(context.Background())
w.runningCtx = runningCtx
go func() {
fn()
doneRunning()
}()
// either the function finishes or a cancel/stop signal is received
return w.wait()
}
func (w *operationWaiter) wait() bool {
log.Printf("[TRACE] TestFileRunner: waiting for execution during %s", w.identifier)
for !w.finished {
select {
case <-time.After(2 * time.Second):
w.updateProgress()
case <-w.evalCtx.stopContext.Done():
// Soft cancel - wait for completion or hard cancel
for !w.finished {
select {
case <-time.After(2 * time.Second):
w.updateProgress()
case <-w.evalCtx.cancelContext.Done():
return w.handleCancelled()
case <-w.runningCtx.Done():
w.finished = true
}
}
case <-w.evalCtx.cancelContext.Done():
return w.handleCancelled()
case <-w.runningCtx.Done():
w.finished = true
}
}
return false
}
// update refreshes the operationWaiter with the latest terraform context, progress, and any newly created resources.
// This should be called before starting a new Terraform operation.
func (w *operationWaiter) update(ctx *terraform.Context, progress moduletest.Progress, created []*plans.ResourceInstanceChangeSrc) {
w.ctx = ctx
w.progress.Store(progress)
w.created = created
}
func (w *operationWaiter) updateProgress() {
now := time.Now().UTC().UnixMilli()
progress := w.progress.Load()
w.renderer.Run(w.run, w.file, progress, now-w.start)
}
// handleCancelled is called when the test execution is hard cancelled.
func (w *operationWaiter) handleCancelled() bool {
log.Printf("[DEBUG] TestFileRunner: test execution cancelled during %s", w.identifier)
states := make(map[string]*states.State)
states[configs.TestMainStateIdentifier] = w.evalCtx.GetState(configs.TestMainStateIdentifier).State
for key, module := range w.evalCtx.FileStates {
if key == configs.TestMainStateIdentifier {
continue
}
states[key] = module.State
}
w.renderer.FatalInterruptSummary(w.run, w.file, states, w.created)
go func() {
if w.ctx != nil {
w.ctx.Stop()
}
}()
for !w.finished {
select {
case <-time.After(2 * time.Second):
w.updateProgress()
case <-w.runningCtx.Done():
w.finished = true
}
}
return true
}