mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
* fix: Allow terraform provisions to be gracefully cancelled This change allows terraform commands to be gracefully cancelled on Unix-like platforms by signaling interrupt on provision cancellation. One implementation detail to note is that we do not necessarily kill a running terraform command immediately even if the stream is closed. The reason for this is to allow for graceful cancellation even in such an event. Currently the timeout is set to 5 minutes by default. Related: #2683 The above issue may be partially or fully fixed by this change. * fix: Remove incorrect minimumTerraformVersion variable * Allow init to return provision complete response
142 lines
3.9 KiB
Go
142 lines
3.9 KiB
Go
package terraform
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/cli/safeexec"
|
|
"github.com/hashicorp/go-version"
|
|
"github.com/hashicorp/hc-install/product"
|
|
"github.com/hashicorp/hc-install/releases"
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog"
|
|
"github.com/coder/coder/provisionersdk"
|
|
)
|
|
|
|
var (
|
|
// TerraformVersion is the version of Terraform used internally
|
|
// when Terraform is not available on the system.
|
|
TerraformVersion = version.Must(version.NewVersion("1.2.1"))
|
|
|
|
minTerraformVersion = version.Must(version.NewVersion("1.1.0"))
|
|
maxTerraformVersion = version.Must(version.NewVersion("1.2.1"))
|
|
|
|
terraformMinorVersionMismatch = xerrors.New("Terraform binary minor version mismatch.")
|
|
)
|
|
|
|
const (
|
|
defaultExitTimeout = 5 * time.Minute
|
|
)
|
|
|
|
type ServeOptions struct {
|
|
*provisionersdk.ServeOptions
|
|
|
|
// BinaryPath specifies the "terraform" binary to use.
|
|
// If omitted, the $PATH will attempt to find it.
|
|
BinaryPath string
|
|
CachePath string
|
|
Logger slog.Logger
|
|
|
|
// ExitTimeout defines how long we will wait for a running Terraform
|
|
// command to exit (cleanly) if the provision was stopped. This only
|
|
// happens when the command is still running after the provision
|
|
// stream is closed. If the provision is canceled via RPC, this
|
|
// timeout will not be used.
|
|
//
|
|
// This is a no-op on Windows where the process can't be interrupted.
|
|
//
|
|
// Default value: 5 minutes.
|
|
ExitTimeout time.Duration
|
|
}
|
|
|
|
func absoluteBinaryPath(ctx context.Context) (string, error) {
|
|
binaryPath, err := safeexec.LookPath("terraform")
|
|
if err != nil {
|
|
return "", xerrors.Errorf("Terraform binary not found: %w", err)
|
|
}
|
|
|
|
// If the "coder" binary is in the same directory as
|
|
// the "terraform" binary, "terraform" is returned.
|
|
//
|
|
// We must resolve the absolute path for other processes
|
|
// to execute this properly!
|
|
absoluteBinary, err := filepath.Abs(binaryPath)
|
|
if err != nil {
|
|
return "", xerrors.Errorf("Terraform binary absolute path not found: %w", err)
|
|
}
|
|
|
|
// Checking the installed version of Terraform.
|
|
version, err := versionFromBinaryPath(ctx, absoluteBinary)
|
|
if err != nil {
|
|
return "", xerrors.Errorf("Terraform binary get version failed: %w", err)
|
|
}
|
|
|
|
if version.LessThan(minTerraformVersion) || version.GreaterThan(maxTerraformVersion) {
|
|
return "", terraformMinorVersionMismatch
|
|
}
|
|
|
|
return absoluteBinary, nil
|
|
}
|
|
|
|
// Serve starts a dRPC server on the provided transport speaking Terraform provisioner.
|
|
func Serve(ctx context.Context, options *ServeOptions) error {
|
|
if options.BinaryPath == "" {
|
|
absoluteBinary, err := absoluteBinaryPath(ctx)
|
|
if err != nil {
|
|
// This is an early exit to prevent extra execution in case the context is canceled.
|
|
// It generally happens in unit tests since this method is asynchronous and
|
|
// the unit test kills the app before this is complete.
|
|
if xerrors.Is(err, context.Canceled) {
|
|
return xerrors.Errorf("absolute binary context canceled: %w", err)
|
|
}
|
|
|
|
installer := &releases.ExactVersion{
|
|
InstallDir: options.CachePath,
|
|
Product: product.Terraform,
|
|
Version: TerraformVersion,
|
|
}
|
|
|
|
execPath, err := installer.Install(ctx)
|
|
if err != nil {
|
|
return xerrors.Errorf("install terraform: %w", err)
|
|
}
|
|
options.BinaryPath = execPath
|
|
} else {
|
|
options.BinaryPath = absoluteBinary
|
|
}
|
|
}
|
|
if options.ExitTimeout == 0 {
|
|
options.ExitTimeout = defaultExitTimeout
|
|
}
|
|
return provisionersdk.Serve(ctx, &server{
|
|
binaryPath: options.BinaryPath,
|
|
cachePath: options.CachePath,
|
|
logger: options.Logger,
|
|
exitTimeout: options.ExitTimeout,
|
|
}, options.ServeOptions)
|
|
}
|
|
|
|
type server struct {
|
|
// initMu protects against executors running `terraform init`
|
|
// concurrently when cache path is set.
|
|
initMu sync.Mutex
|
|
|
|
binaryPath string
|
|
cachePath string
|
|
logger slog.Logger
|
|
|
|
exitTimeout time.Duration
|
|
}
|
|
|
|
func (s *server) executor(workdir string) executor {
|
|
return executor{
|
|
initMu: &s.initMu,
|
|
binaryPath: s.binaryPath,
|
|
cachePath: s.cachePath,
|
|
workdir: workdir,
|
|
}
|
|
}
|