Skip to content

Commit

Permalink
terraform: Check and constrain TF version to >=0.12.0
Browse files Browse the repository at this point in the history
The (currently simple) discovery *and* checking currently happens
during textDocument/completion, which is suboptimal from performance
and responsibility perspective, but it is an acceptable technical debt
as #16 will address that.

Closes #7
  • Loading branch information
radeksimko committed Mar 11, 2020
1 parent 91e330a commit b57790b
Show file tree
Hide file tree
Showing 16 changed files with 301 additions and 113 deletions.
9 changes: 8 additions & 1 deletion commands/completion_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

lsctx "github.com/hashicorp/terraform-ls/internal/context"
"github.com/hashicorp/terraform-ls/internal/filesystem"
"github.com/hashicorp/terraform-ls/internal/terraform/discovery"
"github.com/hashicorp/terraform-ls/internal/terraform/exec"
"github.com/hashicorp/terraform-ls/langserver/handlers"
"github.com/mitchellh/cli"
Expand Down Expand Up @@ -66,9 +67,15 @@ func (c *CompletionCommand) Run(args []string) int {
Version: 0,
})

tfPath, err := discovery.LookPath()
if err != nil {
c.Ui.Error(err.Error())
return 1
}

ctx := context.Background()
ctx = lsctx.WithFilesystem(fs, ctx)
ctx = lsctx.WithTerraformExecutor(exec.NewExecutor(ctx), ctx)
ctx = lsctx.WithTerraformExecutor(exec.NewExecutor(ctx, tfPath), ctx)
ctx = lsctx.WithClientCapabilities(&lsp.ClientCapabilities{}, ctx)

h := handlers.LogHandler(c.Logger)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/apparentlymart/go-textseg v1.0.0
github.com/creachadair/jrpc2 v0.6.1
github.com/google/go-cmp v0.4.0
github.com/hashicorp/go-version v1.2.0
github.com/hashicorp/hcl/v2 v2.3.0
github.com/hashicorp/terraform-json v0.4.0
github.com/mitchellh/cli v1.0.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl/v2 v2.3.0 h1:iRly8YaMwTBAKhn1Ybk7VSdzbnopghktCD031P8ggUE=
github.com/hashicorp/hcl/v2 v2.3.0/go.mod h1:d+FwDBbOLvpAM3Z6J7gPj/VoAGkNe/gm352ZhjJ/Zv8=
github.com/hashicorp/terraform-json v0.4.0 h1:KNh29iNxozP5adfUFBJ4/fWd0Cu3taGgjHB38JYqOF4=
Expand Down
14 changes: 14 additions & 0 deletions internal/terraform/discovery/discovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package discovery

import (
"fmt"
"os/exec"
)

func LookPath() (string, error) {
path, err := exec.LookPath("terraform")
if err != nil {
return "", fmt.Errorf("unable to find terraform: %s", err)
}
return path, nil
}
32 changes: 15 additions & 17 deletions internal/terraform/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,34 @@ import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"time"

tfjson "github.com/hashicorp/terraform-json"
)

// cmdCtxFunc allows mocking of Terraform in tests while retaining
// ability to pass context for timeout/cancellation
type cmdCtxFunc func(context.Context, string, ...string) *exec.Cmd

type Executor struct {
ctx context.Context
timeout time.Duration
workDir string
logger *log.Logger

execPath string
workDir string
logger *log.Logger

cmdCtxFunc cmdCtxFunc
}

func NewExecutor(ctx context.Context) *Executor {
func NewExecutor(ctx context.Context, path string) *Executor {
return &Executor{
ctx: ctx,
timeout: 10 * time.Second,
logger: log.New(ioutil.Discard, "", 0),
ctx: ctx,
timeout: 10 * time.Second,
execPath: path,
logger: log.New(ioutil.Discard, "", 0),
cmdCtxFunc: func(ctx context.Context, path string, arg ...string) *exec.Cmd {
return exec.CommandContext(ctx, path, arg...)
},
Expand Down Expand Up @@ -61,20 +65,14 @@ func (e *Executor) run(args ...string) ([]byte, error) {
var outBuf bytes.Buffer
var errBuf bytes.Buffer

path, err := exec.LookPath("terraform")
if err != nil {
e.logger.Printf("[ERROR] Unable to find terraform with PATH set to %q", os.Getenv("PATH"))
return nil, fmt.Errorf("unable to find terraform for %q: %s", e.workDir, err)
}

cmd := e.cmdCtxFunc(ctx, path, args...)
cmd := e.cmdCtxFunc(ctx, e.execPath, args...)
cmd.Args = append([]string{"terraform"}, args...)
cmd.Dir = e.workDir
cmd.Stderr = &errBuf
cmd.Stdout = &outBuf

e.logger.Printf("Running %s %q in %q...", path, args, e.workDir)
err = cmd.Run()
e.logger.Printf("Running %s %q in %q...", e.execPath, args, e.workDir)
err := cmd.Run()
if err != nil {
if tErr, ok := err.(*exec.ExitError); ok {
exitErr := &ExitError{
Expand All @@ -100,7 +98,7 @@ func (e *Executor) run(args ...string) ([]byte, error) {

pc := cmd.ProcessState
e.logger.Printf("terraform run (%s %q, in %q, pid %d) finished with exit code %d",
path, args, e.workDir, pc.Pid(), pc.ExitCode())
e.execPath, args, e.workDir, pc.Pid(), pc.ExitCode())

return outBuf.Bytes(), nil
}
Expand Down
76 changes: 76 additions & 0 deletions internal/terraform/exec/exec_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package exec

import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"reflect"
"time"
)

type Mock struct {
Args []string `json:"args"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
SleepDuration time.Duration `json:"sleep"`
ExitCode int `json:"exit_code"`
}

func (m *Mock) MarshalJSON() ([]byte, error) {
type t Mock
return json.Marshal((*t)(m))
}

func (m *Mock) UnmarshalJSON(b []byte) error {
type t Mock
return json.Unmarshal(b, (*t)(m))
}

func MockExecutor(m *Mock) *Executor {
if m == nil {
m = &Mock{}
}

path, ctxFunc := mockCommandCtxFunc(m)
executor := NewExecutor(context.Background(), path)
executor.cmdCtxFunc = ctxFunc
return executor
}

func mockCommandCtxFunc(e *Mock) (string, cmdCtxFunc) {
return os.Args[0], func(ctx context.Context, path string, arg ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, os.Args[0], os.Args[1:]...)

expectedJson, _ := e.MarshalJSON()
cmd.Env = []string{"TF_LS_MOCK=" + string(expectedJson)}

return cmd
}
}

func ExecuteMock(rawMockData string) int {
e := &Mock{}
err := e.UnmarshalJSON([]byte(rawMockData))
if err != nil {
fmt.Fprint(os.Stderr, "unable to unmarshal mock response")
return 1
}

givenArgs := os.Args[1:]
if !reflect.DeepEqual(e.Args, givenArgs) {
fmt.Fprintf(os.Stderr, "arguments don't match.\nexpected: %q\ngiven: %q\n",
e.Args, givenArgs)
return 1
}

if e.SleepDuration > 0 {
time.Sleep(e.SleepDuration)
}

fmt.Fprint(os.Stdout, e.Stdout)
fmt.Fprint(os.Stderr, e.Stderr)

return e.ExitCode
}
58 changes: 1 addition & 57 deletions internal/terraform/exec/exec_mock_test.go
Original file line number Diff line number Diff line change
@@ -1,69 +1,13 @@
package exec

import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"reflect"
"testing"
"time"
)

type expected struct {
Args []string `json:"args"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
SleepDuration time.Duration `json:"sleep"`
ExitCode int `json:"exit_code"`
}

func (e *expected) MarshalJSON() ([]byte, error) {
type t expected
return json.Marshal((*t)(e))
}

func (e *expected) UnmarshalJSON(b []byte) error {
type t expected
return json.Unmarshal(b, (*t)(e))
}

func mockCommandCtxFunc(e *expected) cmdCtxFunc {
return func(ctx context.Context, path string, arg ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, os.Args[0], os.Args[1:]...)

expectedJson, _ := e.MarshalJSON()
cmd.Env = []string{"TF_LS_MOCK=" + string(expectedJson)}

return cmd
}
}

func TestMain(m *testing.M) {
if v := os.Getenv("TF_LS_MOCK"); v != "" {
e := &expected{}
err := e.UnmarshalJSON([]byte(v))
if err != nil {
fmt.Fprint(os.Stderr, "unable to unmarshal mock response")
os.Exit(1)
}

givenArgs := os.Args[1:]
if !reflect.DeepEqual(e.Args, givenArgs) {
fmt.Fprintf(os.Stderr, "arguments don't match.\nexpected: %q\ngiven: %q\n",
e.Args, givenArgs)
os.Exit(1)
}

if e.SleepDuration > 0 {
time.Sleep(e.SleepDuration)
}

fmt.Fprint(os.Stdout, e.Stdout)
fmt.Fprint(os.Stderr, e.Stderr)

os.Exit(e.ExitCode)
os.Exit(ExecuteMock(v))
return
}

Expand Down
13 changes: 3 additions & 10 deletions internal/terraform/exec/exec_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package exec

import (
"context"
"errors"
"testing"
"time"
)

func TestExec_timeout(t *testing.T) {
e := mockExecutor(&expected{
e := MockExecutor(&Mock{
Args: []string{"version"},
SleepDuration: 100 * time.Millisecond,
Stdout: "Terraform v0.12.0\n",
Expand All @@ -31,7 +30,7 @@ func TestExec_timeout(t *testing.T) {
}

func TestExec_Version(t *testing.T) {
e := mockExecutor(&expected{
e := MockExecutor(&Mock{
Args: []string{"version"},
Stdout: "Terraform v0.12.0\n",
ExitCode: 0,
Expand All @@ -46,7 +45,7 @@ func TestExec_Version(t *testing.T) {
}

func TestExec_ProviderSchemas(t *testing.T) {
e := mockExecutor(&expected{
e := MockExecutor(&Mock{
Args: []string{"providers", "schema", "-json"},
Stdout: `{"format_version": "0.1"}`,
ExitCode: 0,
Expand All @@ -63,9 +62,3 @@ func TestExec_ProviderSchemas(t *testing.T) {
expectedVersion, ps.FormatVersion)
}
}

func mockExecutor(e *expected) *Executor {
executor := NewExecutor(context.Background())
executor.cmdCtxFunc = mockCommandCtxFunc(e)
return executor
}
40 changes: 37 additions & 3 deletions internal/terraform/lang/config_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io/ioutil"
"log"

"github.com/hashicorp/go-version"
hcl "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
tfjson "github.com/hashicorp/terraform-json"
Expand All @@ -23,13 +24,46 @@ type configBlockFactory interface {
InitializeCapabilities(lsp.TextDocumentClientCapabilities)
}

type Parser interface {
SetLogger(*log.Logger)
SetCapabilities(lsp.TextDocumentClientCapabilities)
ParseBlockFromHCL(*hcl.Block) (ConfigBlock, error)
}

type parser struct {
logger *log.Logger
caps lsp.TextDocumentClientCapabilities
}

func NewParser(logger *log.Logger, caps lsp.TextDocumentClientCapabilities) *parser {
return &parser{logger: logger, caps: caps}
func FindCompatibleParser(v string) (Parser, error) {
tfVersion, err := version.NewVersion(v)
if err != nil {
return nil, err
}
supported, err := version.NewConstraint(">= 0.12.0")
if err != nil {
return nil, err
}

if !supported.Check(tfVersion) {
return nil, fmt.Errorf("No parser available for version %q (%s required)",
tfVersion, supported.String())
}
return newParser(), nil
}

func newParser() *parser {
return &parser{
logger: log.New(ioutil.Discard, "", 0),
}
}

func (p *parser) SetLogger(logger *log.Logger) {
p.logger = logger
}

func (p *parser) SetCapabilities(caps lsp.TextDocumentClientCapabilities) {
p.caps = caps
}

func (p *parser) blockTypes() map[string]configBlockFactory {
Expand All @@ -42,7 +76,7 @@ func (p *parser) blockTypes() map[string]configBlockFactory {
}
}

func (p *parser) ParseBlockFromHcl(block *hcl.Block) (ConfigBlock, error) {
func (p *parser) ParseBlockFromHCL(block *hcl.Block) (ConfigBlock, error) {
f, ok := p.blockTypes()[block.Type]
if !ok {
return nil, fmt.Errorf("unknown block type: %q", block.Type)
Expand Down
Loading

0 comments on commit b57790b

Please sign in to comment.