Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions provisioner/terraform/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package terraform

import (
"context"
"os"
"path/filepath"
"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"
)

var (
// TerraformVersion is the version of Terraform used internally
// when Terraform is not available on the system.
TerraformVersion = version.Must(version.NewVersion("1.3.0"))

minTerraformVersion = version.Must(version.NewVersion("1.1.0"))
maxTerraformVersion = version.Must(version.NewVersion("1.3.0"))

terraformMinorVersionMismatch = xerrors.New("Terraform binary minor version mismatch.")
)

// Install implements a thread-safe, idempotent Terraform Install
// operation.
func Install(ctx context.Context, log slog.Logger, dir string, wantVersion *version.Version) (string, error) {
err := os.MkdirAll(dir, 0750)
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())

hasVersion, err := versionFromBinaryPath(ctx, binPath)
if err == nil && hasVersion.Equal(wantVersion) {
return binPath, err
}

installer := &releases.ExactVersion{
InstallDir: dir,
Product: product.Terraform,
Version: TerraformVersion,
}
installer.SetLogger(slog.Stdlib(ctx, log, slog.LevelDebug))
log.Debug(
ctx,
"installing terraform",
slog.F("dir", dir),
slog.F("version", TerraformVersion),
)

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)
}

return path, nil
}
71 changes: 71 additions & 0 deletions provisioner/terraform/install_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package terraform_test

import (
"context"
"os"
"sync"
"sync/atomic"
"testing"
"time"

"github.com/hashicorp/go-version"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/provisioner/terraform"
)

func TestInstall(t *testing.T) {
t.Parallel()
ctx := context.Background()
dir := t.TempDir()
log := slogtest.Make(t, nil)

// install spins off 8 installs with Version and waits for them all
// to complete.
install := func(version *version.Version) string {
var wg sync.WaitGroup
var path atomic.Pointer[string]
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
p, err := terraform.Install(ctx, log, dir, version)
assert.NoError(t, err)
path.Store(&p)
}()
}
wg.Wait()
if t.Failed() {
t.FailNow()
}
return *path.Load()
}

binPath := install(terraform.TerraformVersion)

checkBin := func() time.Time {
binInfo, err := os.Stat(binPath)
require.NoError(t, err)
require.Greater(t, binInfo.Size(), int64(0))
return binInfo.ModTime()
}

firstMod := checkBin()

// Since we're using the same version the install should be idempotent.
install(terraform.TerraformVersion)
secondMod := checkBin()
require.Equal(t, firstMod, secondMod)

// Ensure a new install happens when version changes
differentVersion := version.Must(version.NewVersion("1.2.0"))
// Sanity-check
require.NotEqual(t, differentVersion.String(), terraform.TerraformVersion.String())

install(differentVersion)

thirdMod := checkBin()
require.Greater(t, thirdMod, secondMod)
}
5 changes: 4 additions & 1 deletion provisioner/terraform/provision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package terraform_test
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -43,7 +44,9 @@ func setupProvisioner(t *testing.T, opts *provisionerServeOptions) (context.Cont
_ = server.Close()
cancelFunc()
err := <-serverErr
assert.NoError(t, err)
if !errors.Is(err, context.Canceled) {
assert.NoError(t, err)
}
})
go func() {
serverErr <- terraform.Serve(ctx, &terraform.ServeOptions{
Expand Down
37 changes: 4 additions & 33 deletions provisioner/terraform/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,12 @@ import (
"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.3.0"))

minTerraformVersion = version.Must(version.NewVersion("1.1.0"))
maxTerraformVersion = version.Must(version.NewVersion("1.3.0"))

terraformMinorVersionMismatch = xerrors.New("Terraform binary minor version mismatch.")

installTerraform sync.Once
installTerraformExecPath string
// nolint:errname
installTerraformError error
)

const (
defaultExitTimeout = 5 * time.Minute
)
Expand Down Expand Up @@ -98,21 +79,11 @@ func Serve(ctx context.Context, options *ServeOptions) error {
return xerrors.Errorf("absolute binary context canceled: %w", err)
}

// We don't want to install Terraform multiple times!
installTerraform.Do(func() {
installer := &releases.ExactVersion{
InstallDir: options.CachePath,
Product: product.Terraform,
Version: TerraformVersion,
}
installer.SetLogger(slog.Stdlib(ctx, options.Logger, slog.LevelDebug))
options.Logger.Debug(ctx, "installing terraform", slog.F("dir", options.CachePath), slog.F("version", TerraformVersion))
installTerraformExecPath, installTerraformError = installer.Install(ctx)
})
if installTerraformError != nil {
return xerrors.Errorf("install terraform: %w", installTerraformError)
binPath, err := Install(ctx, options.Logger, options.CachePath, TerraformVersion)
if err != nil {
return xerrors.Errorf("install terraform: %w", err)
}
options.BinaryPath = installTerraformExecPath
options.BinaryPath = binPath
} else {
options.BinaryPath = absoluteBinary
}
Expand Down