mirror of
https://github.com/hashicorp/terraform.git
synced 2026-03-21 18:10:30 -04:00
163 lines
4.2 KiB
Go
163 lines
4.2 KiB
Go
// Copyright IBM Corp. 2014, 2026
|
|
// 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
|
|
}
|