Skip to content

Commit c6b1daa

Browse files
authored
feat: Download default terraform version when minor version mismatches (#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

Lines changed: 3 additions & 3 deletions
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

Lines changed: 1 addition & 2 deletions
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

Lines changed: 13 additions & 2 deletions
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

Lines changed: 43 additions & 12 deletions
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
}
Lines changed: 98 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy