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

This commit is contained in:
Abhineet Jain 2022-06-22 19:11:52 -04:00 committed by GitHub
parent 6a2a145545
commit c6b1daabc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 158 additions and 19 deletions

View File

@ -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
@ -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
@ -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

View File

@ -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
@ -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
}
}()

View File

@ -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)

View File

@ -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.
@ -31,6 +33,8 @@ var (
}()
)
var terraformMinorVersionMismatch = xerrors.New("Terraform binary minor version mismatch.")
type ServeOptions struct {
*provisionersdk.ServeOptions
@ -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)
@ -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
}
}

View File

@ -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())
}
})
}
}