Skip to content

Commit 795ed3d

Browse files
authored
provisioner: fix multi-dir installs (#4690)
In the previous implementation, tests would occasionally fail since the original install directory was deleted.
1 parent d0fb054 commit 795ed3d

File tree

4 files changed

+159
-34
lines changed

4 files changed

+159
-34
lines changed

provisioner/terraform/install.go

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package terraform
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"time"
8+
9+
"github.com/gofrs/flock"
10+
"github.com/hashicorp/go-version"
11+
"github.com/hashicorp/hc-install/product"
12+
"github.com/hashicorp/hc-install/releases"
13+
"golang.org/x/xerrors"
14+
15+
"cdr.dev/slog"
16+
)
17+
18+
var (
19+
// TerraformVersion is the version of Terraform used internally
20+
// when Terraform is not available on the system.
21+
TerraformVersion = version.Must(version.NewVersion("1.3.0"))
22+
23+
minTerraformVersion = version.Must(version.NewVersion("1.1.0"))
24+
maxTerraformVersion = version.Must(version.NewVersion("1.3.0"))
25+
26+
terraformMinorVersionMismatch = xerrors.New("Terraform binary minor version mismatch.")
27+
)
28+
29+
// Install implements a thread-safe, idempotent Terraform Install
30+
// operation.
31+
func Install(ctx context.Context, log slog.Logger, dir string, wantVersion *version.Version) (string, error) {
32+
err := os.MkdirAll(dir, 0750)
33+
if err != nil {
34+
return "", err
35+
}
36+
37+
// Windows requires a separate lock file.
38+
// See https://github.com/pinterest/knox/blob/master/client/flock_windows.go#L64
39+
// for precedent.
40+
lockFilePath := filepath.Join(dir, "lock")
41+
lock := flock.New(lockFilePath)
42+
ok, err := lock.TryLockContext(ctx, time.Millisecond*100)
43+
if !ok {
44+
return "", xerrors.Errorf("could not acquire flock for %v: %w", lockFilePath, err)
45+
}
46+
defer lock.Close()
47+
48+
binPath := filepath.Join(dir, product.Terraform.BinaryName())
49+
50+
hasVersion, err := versionFromBinaryPath(ctx, binPath)
51+
if err == nil && hasVersion.Equal(wantVersion) {
52+
return binPath, err
53+
}
54+
55+
installer := &releases.ExactVersion{
56+
InstallDir: dir,
57+
Product: product.Terraform,
58+
Version: TerraformVersion,
59+
}
60+
installer.SetLogger(slog.Stdlib(ctx, log, slog.LevelDebug))
61+
log.Debug(
62+
ctx,
63+
"installing terraform",
64+
slog.F("dir", dir),
65+
slog.F("version", TerraformVersion),
66+
)
67+
68+
path, err := installer.Install(ctx)
69+
if err != nil {
70+
return "", xerrors.Errorf("install: %w", err)
71+
}
72+
73+
// Sanity-check: if path != binPath then future invocations of Install
74+
// will fail.
75+
if path != binPath {
76+
return "", xerrors.Errorf("%s should be %s", path, binPath)
77+
}
78+
79+
return path, nil
80+
}

provisioner/terraform/install_test.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package terraform_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"sync"
7+
"sync/atomic"
8+
"testing"
9+
"time"
10+
11+
"github.com/hashicorp/go-version"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
15+
"cdr.dev/slog/sloggers/slogtest"
16+
"github.com/coder/coder/provisioner/terraform"
17+
)
18+
19+
func TestInstall(t *testing.T) {
20+
t.Parallel()
21+
ctx := context.Background()
22+
dir := t.TempDir()
23+
log := slogtest.Make(t, nil)
24+
25+
// install spins off 8 installs with Version and waits for them all
26+
// to complete.
27+
install := func(version *version.Version) string {
28+
var wg sync.WaitGroup
29+
var path atomic.Pointer[string]
30+
for i := 0; i < 8; i++ {
31+
wg.Add(1)
32+
go func() {
33+
defer wg.Done()
34+
p, err := terraform.Install(ctx, log, dir, version)
35+
assert.NoError(t, err)
36+
path.Store(&p)
37+
}()
38+
}
39+
wg.Wait()
40+
if t.Failed() {
41+
t.FailNow()
42+
}
43+
return *path.Load()
44+
}
45+
46+
binPath := install(terraform.TerraformVersion)
47+
48+
checkBin := func() time.Time {
49+
binInfo, err := os.Stat(binPath)
50+
require.NoError(t, err)
51+
require.Greater(t, binInfo.Size(), int64(0))
52+
return binInfo.ModTime()
53+
}
54+
55+
firstMod := checkBin()
56+
57+
// Since we're using the same version the install should be idempotent.
58+
install(terraform.TerraformVersion)
59+
secondMod := checkBin()
60+
require.Equal(t, firstMod, secondMod)
61+
62+
// Ensure a new install happens when version changes
63+
differentVersion := version.Must(version.NewVersion("1.2.0"))
64+
// Sanity-check
65+
require.NotEqual(t, differentVersion.String(), terraform.TerraformVersion.String())
66+
67+
install(differentVersion)
68+
69+
thirdMod := checkBin()
70+
require.Greater(t, thirdMod, secondMod)
71+
}

provisioner/terraform/provision_test.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package terraform_test
55
import (
66
"context"
77
"encoding/json"
8+
"errors"
89
"fmt"
910
"os"
1011
"path/filepath"
@@ -43,7 +44,9 @@ func setupProvisioner(t *testing.T, opts *provisionerServeOptions) (context.Cont
4344
_ = server.Close()
4445
cancelFunc()
4546
err := <-serverErr
46-
assert.NoError(t, err)
47+
if !errors.Is(err, context.Canceled) {
48+
assert.NoError(t, err)
49+
}
4750
})
4851
go func() {
4952
serverErr <- terraform.Serve(ctx, &terraform.ServeOptions{

provisioner/terraform/serve.go

+4-33
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,12 @@ import (
77
"time"
88

99
"github.com/cli/safeexec"
10-
"github.com/hashicorp/go-version"
11-
"github.com/hashicorp/hc-install/product"
12-
"github.com/hashicorp/hc-install/releases"
1310
"golang.org/x/xerrors"
1411

1512
"cdr.dev/slog"
1613
"github.com/coder/coder/provisionersdk"
1714
)
1815

19-
var (
20-
// TerraformVersion is the version of Terraform used internally
21-
// when Terraform is not available on the system.
22-
TerraformVersion = version.Must(version.NewVersion("1.3.0"))
23-
24-
minTerraformVersion = version.Must(version.NewVersion("1.1.0"))
25-
maxTerraformVersion = version.Must(version.NewVersion("1.3.0"))
26-
27-
terraformMinorVersionMismatch = xerrors.New("Terraform binary minor version mismatch.")
28-
29-
installTerraform sync.Once
30-
installTerraformExecPath string
31-
// nolint:errname
32-
installTerraformError error
33-
)
34-
3516
const (
3617
defaultExitTimeout = 5 * time.Minute
3718
)
@@ -98,21 +79,11 @@ func Serve(ctx context.Context, options *ServeOptions) error {
9879
return xerrors.Errorf("absolute binary context canceled: %w", err)
9980
}
10081

101-
// We don't want to install Terraform multiple times!
102-
installTerraform.Do(func() {
103-
installer := &releases.ExactVersion{
104-
InstallDir: options.CachePath,
105-
Product: product.Terraform,
106-
Version: TerraformVersion,
107-
}
108-
installer.SetLogger(slog.Stdlib(ctx, options.Logger, slog.LevelDebug))
109-
options.Logger.Debug(ctx, "installing terraform", slog.F("dir", options.CachePath), slog.F("version", TerraformVersion))
110-
installTerraformExecPath, installTerraformError = installer.Install(ctx)
111-
})
112-
if installTerraformError != nil {
113-
return xerrors.Errorf("install terraform: %w", installTerraformError)
82+
binPath, err := Install(ctx, options.Logger, options.CachePath, TerraformVersion)
83+
if err != nil {
84+
return xerrors.Errorf("install terraform: %w", err)
11485
}
115-
options.BinaryPath = installTerraformExecPath
86+
options.BinaryPath = binPath
11687
} else {
11788
options.BinaryPath = absoluteBinary
11889
}

0 commit comments

Comments
 (0)