Files
coder/provisioner/terraform/install.go
T
Lukasz 4c0c621f2a chore: bump bundled terraform to 1.14.5 (#22167)
Description:
This PR updates the bundled Terraform binary and related version pins
from 1.14.1 to 1.14.5 (base image, installer fallback, and CI/test
fixtures). Terraform is statically built with an embedded Go runtime.
Moving to 1.14.5 updates the embedded toolchain and is intended to
address Go stdlib CVEs reported by security scanning.

Notes:
- Change is version-only; no functional Coder logic changes.
- Backport-friendly: intended to be cherry-picked to release branches
after merge.
2026-02-18 12:18:38 +01:00

123 lines
3.3 KiB
Go

package terraform
import (
"context"
"fmt"
"os"
"path/filepath"
"sync/atomic"
"time"
"github.com/gofrs/flock"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hc-install/product"
"github.com/hashicorp/hc-install/releases"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
)
var (
// TerraformVersion is the version of Terraform used internally
// when Terraform is not available on the system.
// NOTE: Keep this in sync with the version in scripts/Dockerfile.base.
// NOTE: Keep this in sync with the version in install.sh.
TerraformVersion = version.Must(version.NewVersion("1.14.5"))
minTerraformVersion = version.Must(version.NewVersion("1.1.0"))
maxTerraformVersion = version.Must(version.NewVersion("1.14.9")) // use .9 to automatically allow patch releases
errTerraformMinorVersionMismatch = xerrors.New("Terraform binary minor version mismatch.")
)
// Install implements a thread-safe, idempotent Terraform Install
// operation.
//
//nolint:revive // verbose is a control flag that controls the verbosity of the log output.
func Install(ctx context.Context, log slog.Logger, verbose bool, dir string, wantVersion *version.Version, baseUrl string) (string, error) {
err := os.MkdirAll(dir, 0o750)
if err != nil {
return "", err
}
// Windows requires a separate lock file.
// See https://github.com/pinterest/knox/blob/master/client/flock_windows.go#L64
// for precedent.
lockFilePath := filepath.Join(dir, "lock")
lock := flock.New(lockFilePath)
ok, err := lock.TryLockContext(ctx, time.Millisecond*100)
if !ok {
return "", xerrors.Errorf("could not acquire flock for %v: %w", lockFilePath, err)
}
defer lock.Close()
binPath := filepath.Join(dir, product.Terraform.BinaryName())
hasVersionStr := "nil"
hasVersion, err := versionFromBinaryPath(ctx, binPath)
if err == nil {
hasVersionStr = hasVersion.String()
if hasVersion.Equal(wantVersion) {
return binPath, err
}
}
installer := &releases.ExactVersion{
InstallDir: dir,
Product: product.Terraform,
Version: TerraformVersion,
}
installer.SetLogger(slog.Stdlib(ctx, log, slog.LevelDebug))
if baseUrl != "" {
installer.ApiBaseURL = baseUrl
}
logInstall := log.Debug
if verbose {
logInstall = log.Info
}
logInstall(ctx, "installing terraform",
slog.F("prev_version", hasVersionStr),
slog.F("dir", dir),
slog.F("version", TerraformVersion))
prolongedInstall := atomic.Bool{}
prolongedInstallCtx, prolongedInstallCancel := context.WithCancel(ctx)
go func() {
seconds := 15
select {
case <-time.After(time.Duration(seconds) * time.Second):
prolongedInstall.Store(true)
// We always want to log this at the info level.
log.Info(
prolongedInstallCtx,
fmt.Sprintf("terraform installation is taking longer than %d seconds, still in progress", seconds),
slog.F("prev_version", hasVersionStr),
slog.F("dir", dir),
slog.F("version", TerraformVersion),
)
case <-prolongedInstallCtx.Done():
return
}
}()
defer prolongedInstallCancel()
path, err := installer.Install(ctx)
if err != nil {
return "", xerrors.Errorf("install: %w", err)
}
// Sanity-check: if path != binPath then future invocations of Install
// will fail.
if path != binPath {
return "", xerrors.Errorf("%s should be %s", path, binPath)
}
if prolongedInstall.Load() {
log.Info(ctx, "terraform installation complete")
}
return path, nil
}