mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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
|
||||||
|
}
|
||||||
@@ -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,6 +5,7 @@ package terraform_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -43,7 +44,9 @@ func setupProvisioner(t *testing.T, opts *provisionerServeOptions) (context.Cont
|
|||||||
_ = server.Close()
|
_ = server.Close()
|
||||||
cancelFunc()
|
cancelFunc()
|
||||||
err := <-serverErr
|
err := <-serverErr
|
||||||
assert.NoError(t, err)
|
if !errors.Is(err, context.Canceled) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
go func() {
|
go func() {
|
||||||
serverErr <- terraform.Serve(ctx, &terraform.ServeOptions{
|
serverErr <- terraform.Serve(ctx, &terraform.ServeOptions{
|
||||||
|
|||||||
@@ -7,31 +7,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cli/safeexec"
|
"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"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
"github.com/coder/coder/provisionersdk"
|
"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 (
|
const (
|
||||||
defaultExitTimeout = 5 * time.Minute
|
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)
|
return xerrors.Errorf("absolute binary context canceled: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't want to install Terraform multiple times!
|
binPath, err := Install(ctx, options.Logger, options.CachePath, TerraformVersion)
|
||||||
installTerraform.Do(func() {
|
if err != nil {
|
||||||
installer := &releases.ExactVersion{
|
return xerrors.Errorf("install terraform: %w", err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
options.BinaryPath = installTerraformExecPath
|
options.BinaryPath = binPath
|
||||||
} else {
|
} else {
|
||||||
options.BinaryPath = absoluteBinary
|
options.BinaryPath = absoluteBinary
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user