provisioner: fix multi-dir installs (#4690)

In the previous implementation, tests would occasionally fail since the original install directory was deleted.
This commit is contained in:
Ammar Bandukwala
2022-10-22 20:44:05 +00:00
committed by GitHub
parent d0fb054a55
commit 795ed3dc97
4 changed files with 159 additions and 34 deletions
+80
View File
@@ -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
View File
@@ -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)
}
+4 -1
View File
@@ -5,6 +5,7 @@ package terraform_test
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@@ -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{
+4 -33
View File
@@ -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
)
@@ -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
}