Skip to content

feat: Download default terraform version when minor version mismatches #1775

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 22, 2022
6 changes: 3 additions & 3 deletions .github/workflows/coder.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ jobs:

- uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.1.2
terraform_version: 1.1.9
terraform_wrapper: false

- name: Test with Mock Database
Expand Down Expand Up @@ -264,7 +264,7 @@ jobs:

- uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.1.2
terraform_version: 1.1.9
terraform_wrapper: false

- name: Start PostgreSQL Database
Expand Down Expand Up @@ -494,7 +494,7 @@ jobs:

- uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.1.2
terraform_version: 1.1.9
terraform_wrapper: false

- uses: actions/setup-node@v3
Expand Down
3 changes: 1 addition & 2 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,6 @@ func server() *cobra.Command {
shutdownConnsCtx, shutdownConns := context.WithCancel(cmd.Context())
defer shutdownConns()
go func() {
defer close(errCh)
server := http.Server{
// These errors are typically noise like "TLS: EOF". Vault does similar:
// https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714
Expand Down Expand Up @@ -590,7 +589,7 @@ func newProvisionerDaemon(ctx context.Context, coderAPI *coderd.API,
CachePath: cacheDir,
Logger: logger,
})
if err != nil {
if err != nil && !xerrors.Is(err, context.Canceled) {
errChan <- err
}
}()
Expand Down
15 changes: 13 additions & 2 deletions provisioner/terraform/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,22 @@ func (e executor) checkMinVersion(ctx context.Context) error {
}

func (e executor) version(ctx context.Context) (*version.Version, error) {
return versionFromBinaryPath(ctx, e.binaryPath)
}

func versionFromBinaryPath(ctx context.Context, binaryPath string) (*version.Version, error) {
// #nosec
cmd := exec.CommandContext(ctx, e.binaryPath, "version", "-json")
cmd := exec.CommandContext(ctx, binaryPath, "version", "-json")
out, err := cmd.Output()
if err != nil {
return nil, err
select {
// `exec` library throws a `signal: killed`` error instead of the canceled context.
// Since we know the cause for the killed signal, we are throwing the relevant error here.
case <-ctx.Done():
return nil, ctx.Err()
default:
return nil, err
}
}
vj := tfjson.VersionOutput{}
err = json.Unmarshal(out, &vj)
Expand Down
55 changes: 43 additions & 12 deletions provisioner/terraform/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import (

// This is the exact version of Terraform used internally
// when Terraform is missing on the system.
const terraformVersion = "1.1.9"
var terraformVersion = version.Must(version.NewVersion("1.1.9"))
var minTerraformVersion = version.Must(version.NewVersion("1.1.0"))
var maxTerraformVersion = version.Must(version.NewVersion("1.2.0"))

var (
// The minimum version of Terraform supported by the provisioner.
Expand All @@ -31,6 +33,8 @@ var (
}()
)

var terraformMinorVersionMismatch = xerrors.New("Terraform binary minor version mismatch.")

type ServeOptions struct {
*provisionersdk.ServeOptions

Expand All @@ -41,15 +45,51 @@ type ServeOptions struct {
Logger slog.Logger
}

func absoluteBinaryPath(ctx context.Context) (string, error) {
binaryPath, err := safeexec.LookPath("terraform")
if err != nil {
return "", xerrors.Errorf("Terraform binary not found: %w", err)
}

// If the "coder" binary is in the same directory as
// the "terraform" binary, "terraform" is returned.
//
// We must resolve the absolute path for other processes
// to execute this properly!
absoluteBinary, err := filepath.Abs(binaryPath)
if err != nil {
return "", xerrors.Errorf("Terraform binary absolute path not found: %w", err)
}

// Checking the installed version of Terraform.
version, err := versionFromBinaryPath(ctx, absoluteBinary)
if err != nil {
return "", xerrors.Errorf("Terraform binary get version failed: %w", err)
}

if version.LessThan(minTerraformVersion) || version.GreaterThanOrEqual(maxTerraformVersion) {
return "", terraformMinorVersionMismatch
}

return absoluteBinary, nil
}

// Serve starts a dRPC server on the provided transport speaking Terraform provisioner.
func Serve(ctx context.Context, options *ServeOptions) error {
if options.BinaryPath == "" {
binaryPath, err := safeexec.LookPath("terraform")
absoluteBinary, err := absoluteBinaryPath(ctx)
if err != nil {
// This is an early exit to prevent extra execution in case the context is canceled.
// It generally happens in unit tests since this method is asynchronous and
// the unit test kills the app before this is complete.
if xerrors.Is(err, context.Canceled) {
return xerrors.Errorf("absolute binary context canceled: %w", err)
}

installer := &releases.ExactVersion{
InstallDir: options.CachePath,
Product: product.Terraform,
Version: version.Must(version.NewVersion(terraformVersion)),
Version: terraformVersion,
}

execPath, err := installer.Install(ctx)
Expand All @@ -58,15 +98,6 @@ func Serve(ctx context.Context, options *ServeOptions) error {
}
options.BinaryPath = execPath
} else {
// If the "coder" binary is in the same directory as
// the "terraform" binary, "terraform" is returned.
//
// We must resolve the absolute path for other processes
// to execute this properly!
absoluteBinary, err := filepath.Abs(binaryPath)
if err != nil {
return xerrors.Errorf("absolute: %w", err)
}
options.BinaryPath = absoluteBinary
}
}
Expand Down
98 changes: 98 additions & 0 deletions provisioner/terraform/serve_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package terraform

import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
)

// nolint:paralleltest
func Test_absoluteBinaryPath(t *testing.T) {
type args struct {
ctx context.Context
}
tests := []struct {
name string
args args
terraformVersion string
expectedErr error
}{
{
name: "TestCorrectVersion",
args: args{ctx: context.Background()},
terraformVersion: "1.1.9",
expectedErr: nil,
},
{
name: "TestOldVersion",
args: args{ctx: context.Background()},
terraformVersion: "1.0.9",
expectedErr: terraformMinorVersionMismatch,
},
{
name: "TestNewVersion",
args: args{ctx: context.Background()},
terraformVersion: "1.2.9",
expectedErr: terraformMinorVersionMismatch,
},
{
name: "TestMalformedVersion",
args: args{ctx: context.Background()},
terraformVersion: "version",
expectedErr: xerrors.Errorf("Terraform binary get version failed: Malformed version: version"),
},
}
// nolint:paralleltest
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Dummy terraform executable on Windows requires sh which isn't very practical.")
}

// Create a temp dir with the binary
tempDir := t.TempDir()
terraformBinaryOutput := fmt.Sprintf(`#!/bin/sh
cat <<-EOF
{
"terraform_version": "%s",
"platform": "linux_amd64",
"provider_selections": {},
"terraform_outdated": false
}
EOF`, tt.terraformVersion)

// #nosec
err := os.WriteFile(
filepath.Join(tempDir, "terraform"),
[]byte(terraformBinaryOutput),
0770,
)
require.NoError(t, err)

// Add the binary to PATH
pathVariable := os.Getenv("PATH")
t.Setenv("PATH", strings.Join([]string{tempDir, pathVariable}, ":"))

var expectedAbsoluteBinary string
if tt.expectedErr == nil {
expectedAbsoluteBinary = filepath.Join(tempDir, "terraform")
}

actualAbsoluteBinary, actualErr := absoluteBinaryPath(tt.args.ctx)

require.Equal(t, expectedAbsoluteBinary, actualAbsoluteBinary)
if tt.expectedErr == nil {
require.NoError(t, actualErr)
} else {
require.EqualError(t, actualErr, tt.expectedErr.Error())
}
})
}
}