Skip to content

Commit c6b1daa

Browse files
authored
feat: Download default terraform version when minor version mismatches (coder#1775)
1 parent 6a2a145 commit c6b1daa

File tree

5 files changed

+158
-19
lines changed

5 files changed

+158
-19
lines changed

.github/workflows/coder.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ jobs:
197197

198198
- uses: hashicorp/setup-terraform@v2
199199
with:
200-
terraform_version: 1.1.2
200+
terraform_version: 1.1.9
201201
terraform_wrapper: false
202202

203203
- name: Test with Mock Database
@@ -264,7 +264,7 @@ jobs:
264264

265265
- uses: hashicorp/setup-terraform@v2
266266
with:
267-
terraform_version: 1.1.2
267+
terraform_version: 1.1.9
268268
terraform_wrapper: false
269269

270270
- name: Start PostgreSQL Database
@@ -494,7 +494,7 @@ jobs:
494494

495495
- uses: hashicorp/setup-terraform@v2
496496
with:
497-
terraform_version: 1.1.2
497+
terraform_version: 1.1.9
498498
terraform_wrapper: false
499499

500500
- uses: actions/setup-node@v3

cli/server.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,6 @@ func server() *cobra.Command {
376376
shutdownConnsCtx, shutdownConns := context.WithCancel(cmd.Context())
377377
defer shutdownConns()
378378
go func() {
379-
defer close(errCh)
380379
server := http.Server{
381380
// These errors are typically noise like "TLS: EOF". Vault does similar:
382381
// https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714
@@ -590,7 +589,7 @@ func newProvisionerDaemon(ctx context.Context, coderAPI *coderd.API,
590589
CachePath: cacheDir,
591590
Logger: logger,
592591
})
593-
if err != nil {
592+
if err != nil && !xerrors.Is(err, context.Canceled) {
594593
errChan <- err
595594
}
596595
}()

provisioner/terraform/executor.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,22 @@ func (e executor) checkMinVersion(ctx context.Context) error {
104104
}
105105

106106
func (e executor) version(ctx context.Context) (*version.Version, error) {
107+
return versionFromBinaryPath(ctx, e.binaryPath)
108+
}
109+
110+
func versionFromBinaryPath(ctx context.Context, binaryPath string) (*version.Version, error) {
107111
// #nosec
108-
cmd := exec.CommandContext(ctx, e.binaryPath, "version", "-json")
112+
cmd := exec.CommandContext(ctx, binaryPath, "version", "-json")
109113
out, err := cmd.Output()
110114
if err != nil {
111-
return nil, err
115+
select {
116+
// `exec` library throws a `signal: killed`` error instead of the canceled context.
117+
// Since we know the cause for the killed signal, we are throwing the relevant error here.
118+
case <-ctx.Done():
119+
return nil, ctx.Err()
120+
default:
121+
return nil, err
122+
}
112123
}
113124
vj := tfjson.VersionOutput{}
114125
err = json.Unmarshal(out, &vj)

provisioner/terraform/serve.go

+43-12
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import (
1616

1717
// This is the exact version of Terraform used internally
1818
// when Terraform is missing on the system.
19-
const terraformVersion = "1.1.9"
19+
var terraformVersion = version.Must(version.NewVersion("1.1.9"))
20+
var minTerraformVersion = version.Must(version.NewVersion("1.1.0"))
21+
var maxTerraformVersion = version.Must(version.NewVersion("1.2.0"))
2022

2123
var (
2224
// The minimum version of Terraform supported by the provisioner.
@@ -31,6 +33,8 @@ var (
3133
}()
3234
)
3335

36+
var terraformMinorVersionMismatch = xerrors.New("Terraform binary minor version mismatch.")
37+
3438
type ServeOptions struct {
3539
*provisionersdk.ServeOptions
3640

@@ -41,15 +45,51 @@ type ServeOptions struct {
4145
Logger slog.Logger
4246
}
4347

48+
func absoluteBinaryPath(ctx context.Context) (string, error) {
49+
binaryPath, err := safeexec.LookPath("terraform")
50+
if err != nil {
51+
return "", xerrors.Errorf("Terraform binary not found: %w", err)
52+
}
53+
54+
// If the "coder" binary is in the same directory as
55+
// the "terraform" binary, "terraform" is returned.
56+
//
57+
// We must resolve the absolute path for other processes
58+
// to execute this properly!
59+
absoluteBinary, err := filepath.Abs(binaryPath)
60+
if err != nil {
61+
return "", xerrors.Errorf("Terraform binary absolute path not found: %w", err)
62+
}
63+
64+
// Checking the installed version of Terraform.
65+
version, err := versionFromBinaryPath(ctx, absoluteBinary)
66+
if err != nil {
67+
return "", xerrors.Errorf("Terraform binary get version failed: %w", err)
68+
}
69+
70+
if version.LessThan(minTerraformVersion) || version.GreaterThanOrEqual(maxTerraformVersion) {
71+
return "", terraformMinorVersionMismatch
72+
}
73+
74+
return absoluteBinary, nil
75+
}
76+
4477
// Serve starts a dRPC server on the provided transport speaking Terraform provisioner.
4578
func Serve(ctx context.Context, options *ServeOptions) error {
4679
if options.BinaryPath == "" {
47-
binaryPath, err := safeexec.LookPath("terraform")
80+
absoluteBinary, err := absoluteBinaryPath(ctx)
4881
if err != nil {
82+
// This is an early exit to prevent extra execution in case the context is canceled.
83+
// It generally happens in unit tests since this method is asynchronous and
84+
// the unit test kills the app before this is complete.
85+
if xerrors.Is(err, context.Canceled) {
86+
return xerrors.Errorf("absolute binary context canceled: %w", err)
87+
}
88+
4989
installer := &releases.ExactVersion{
5090
InstallDir: options.CachePath,
5191
Product: product.Terraform,
52-
Version: version.Must(version.NewVersion(terraformVersion)),
92+
Version: terraformVersion,
5393
}
5494

5595
execPath, err := installer.Install(ctx)
@@ -58,15 +98,6 @@ func Serve(ctx context.Context, options *ServeOptions) error {
5898
}
5999
options.BinaryPath = execPath
60100
} else {
61-
// If the "coder" binary is in the same directory as
62-
// the "terraform" binary, "terraform" is returned.
63-
//
64-
// We must resolve the absolute path for other processes
65-
// to execute this properly!
66-
absoluteBinary, err := filepath.Abs(binaryPath)
67-
if err != nil {
68-
return xerrors.Errorf("absolute: %w", err)
69-
}
70101
options.BinaryPath = absoluteBinary
71102
}
72103
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package terraform
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"runtime"
9+
"strings"
10+
"testing"
11+
12+
"github.com/stretchr/testify/require"
13+
"golang.org/x/xerrors"
14+
)
15+
16+
// nolint:paralleltest
17+
func Test_absoluteBinaryPath(t *testing.T) {
18+
type args struct {
19+
ctx context.Context
20+
}
21+
tests := []struct {
22+
name string
23+
args args
24+
terraformVersion string
25+
expectedErr error
26+
}{
27+
{
28+
name: "TestCorrectVersion",
29+
args: args{ctx: context.Background()},
30+
terraformVersion: "1.1.9",
31+
expectedErr: nil,
32+
},
33+
{
34+
name: "TestOldVersion",
35+
args: args{ctx: context.Background()},
36+
terraformVersion: "1.0.9",
37+
expectedErr: terraformMinorVersionMismatch,
38+
},
39+
{
40+
name: "TestNewVersion",
41+
args: args{ctx: context.Background()},
42+
terraformVersion: "1.2.9",
43+
expectedErr: terraformMinorVersionMismatch,
44+
},
45+
{
46+
name: "TestMalformedVersion",
47+
args: args{ctx: context.Background()},
48+
terraformVersion: "version",
49+
expectedErr: xerrors.Errorf("Terraform binary get version failed: Malformed version: version"),
50+
},
51+
}
52+
// nolint:paralleltest
53+
for _, tt := range tests {
54+
t.Run(tt.name, func(t *testing.T) {
55+
if runtime.GOOS == "windows" {
56+
t.Skip("Dummy terraform executable on Windows requires sh which isn't very practical.")
57+
}
58+
59+
// Create a temp dir with the binary
60+
tempDir := t.TempDir()
61+
terraformBinaryOutput := fmt.Sprintf(`#!/bin/sh
62+
cat <<-EOF
63+
{
64+
"terraform_version": "%s",
65+
"platform": "linux_amd64",
66+
"provider_selections": {},
67+
"terraform_outdated": false
68+
}
69+
EOF`, tt.terraformVersion)
70+
71+
// #nosec
72+
err := os.WriteFile(
73+
filepath.Join(tempDir, "terraform"),
74+
[]byte(terraformBinaryOutput),
75+
0770,
76+
)
77+
require.NoError(t, err)
78+
79+
// Add the binary to PATH
80+
pathVariable := os.Getenv("PATH")
81+
t.Setenv("PATH", strings.Join([]string{tempDir, pathVariable}, ":"))
82+
83+
var expectedAbsoluteBinary string
84+
if tt.expectedErr == nil {
85+
expectedAbsoluteBinary = filepath.Join(tempDir, "terraform")
86+
}
87+
88+
actualAbsoluteBinary, actualErr := absoluteBinaryPath(tt.args.ctx)
89+
90+
require.Equal(t, expectedAbsoluteBinary, actualAbsoluteBinary)
91+
if tt.expectedErr == nil {
92+
require.NoError(t, actualErr)
93+
} else {
94+
require.EqualError(t, actualErr, tt.expectedErr.Error())
95+
}
96+
})
97+
}
98+
}

0 commit comments

Comments
 (0)