Skip to content

Commit 4f1bd35

Browse files
committed
terraform/schema: Introduce schema caching & refactor langserver tests
1 parent 720724c commit 4f1bd35

21 files changed

+656
-227
lines changed

internal/context/context.go

+62
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/hashicorp/terraform-ls/internal/filesystem"
1111
"github.com/hashicorp/terraform-ls/internal/terraform/exec"
12+
"github.com/hashicorp/terraform-ls/internal/terraform/schema"
1213
"github.com/sourcegraph/go-lsp"
1314
)
1415

@@ -96,3 +97,64 @@ func ClientCapabilities(ctx context.Context) (lsp.ClientCapabilities, error) {
9697

9798
return *caps, nil
9899
}
100+
101+
const ctxTfSchemaWriter = "ctxTerraformSchemaWriter"
102+
103+
func WithTerraformSchemaWriter(s schema.Writer, ctx context.Context) context.Context {
104+
return context.WithValue(ctx, ctxTfSchemaWriter, s)
105+
}
106+
107+
func TerraformSchemaWriter(ctx context.Context) (schema.Writer, error) {
108+
ss, ok := ctx.Value(ctxTfSchemaWriter).(schema.Writer)
109+
if !ok {
110+
return nil, fmt.Errorf("no terraform schema writer")
111+
}
112+
113+
return ss, nil
114+
}
115+
116+
const ctxTfSchemaReader = "ctxTerraformSchemaWriter"
117+
118+
func WithTerraformSchemaReader(s schema.Reader, ctx context.Context) context.Context {
119+
return context.WithValue(ctx, ctxTfSchemaReader, s)
120+
}
121+
122+
func TerraformSchemaReader(ctx context.Context) (schema.Reader, error) {
123+
ss, ok := ctx.Value(ctxTfSchemaReader).(schema.Reader)
124+
if !ok {
125+
return nil, fmt.Errorf("no terraform schema reader")
126+
}
127+
128+
return ss, nil
129+
}
130+
131+
const ctxTfVersion = "ctxTerraformVersion"
132+
133+
func WithTerraformVersion(v string, ctx context.Context) context.Context {
134+
return context.WithValue(ctx, ctxTfVersion, v)
135+
}
136+
137+
func TerraformVersion(ctx context.Context) (string, error) {
138+
tfv, ok := ctx.Value(ctxTfVersion).(string)
139+
if !ok {
140+
return "", fmt.Errorf("no Terraform version")
141+
}
142+
143+
return tfv, nil
144+
}
145+
146+
const ctxTfVersionSetter = "ctxTerraformVersionSetter"
147+
148+
func WithTerraformVersionSetter(v *string, ctx context.Context) context.Context {
149+
return context.WithValue(ctx, ctxTfVersionSetter, v)
150+
}
151+
152+
func SetTerraformVersion(ctx context.Context, v string) error {
153+
tfv, ok := ctx.Value(ctxTfVersionSetter).(*string)
154+
if !ok {
155+
return fmt.Errorf("no Terraform version setter")
156+
}
157+
*tfv = v
158+
159+
return nil
160+
}

internal/terraform/exec/exec.go

+23
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"strings"
1313
"time"
1414

15+
"github.com/hashicorp/go-version"
1516
tfjson "github.com/hashicorp/terraform-json"
1617
)
1718

@@ -55,6 +56,10 @@ func (e *Executor) SetWorkdir(workdir string) {
5556
}
5657

5758
func (e *Executor) run(args ...string) ([]byte, error) {
59+
if e.workDir == "" {
60+
return nil, fmt.Errorf("no work directory set")
61+
}
62+
5863
ctx := e.ctx
5964
if e.timeout > 0 {
6065
var cancel context.CancelFunc
@@ -118,6 +123,24 @@ func (e *Executor) Version() (string, error) {
118123
return version, nil
119124
}
120125

126+
func (e *Executor) VersionIsSupported(c version.Constraints) error {
127+
v, err := e.Version()
128+
if err != nil {
129+
return err
130+
}
131+
ver, err := version.NewVersion(v)
132+
if err != nil {
133+
return err
134+
}
135+
136+
if !c.Check(ver) {
137+
return fmt.Errorf("version %s not supported (%s)",
138+
ver.String(), c.String())
139+
}
140+
141+
return nil
142+
}
143+
121144
func (e *Executor) ProviderSchemas() (*tfjson.ProviderSchemas, error) {
122145
outBytes, err := e.run("providers", "schema", "-json")
123146
if err != nil {

internal/terraform/exec/exec_mock.go

+88-24
Original file line numberDiff line numberDiff line change
@@ -4,73 +4,137 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"os"
89
"os/exec"
910
"reflect"
1011
"time"
1112
)
1213

13-
type Mock struct {
14+
type MockItemDispenser interface {
15+
NextMockItem() *MockItem
16+
}
17+
18+
type MockItem struct {
1419
Args []string `json:"args"`
1520
Stdout string `json:"stdout"`
1621
Stderr string `json:"stderr"`
1722
SleepDuration time.Duration `json:"sleep"`
1823
ExitCode int `json:"exit_code"`
24+
25+
MockError string `json:"error"`
1926
}
2027

21-
func (m *Mock) MarshalJSON() ([]byte, error) {
22-
type t Mock
28+
func (m *MockItem) MarshalJSON() ([]byte, error) {
29+
type t MockItem
2330
return json.Marshal((*t)(m))
2431
}
2532

26-
func (m *Mock) UnmarshalJSON(b []byte) error {
27-
type t Mock
33+
func (m *MockItem) UnmarshalJSON(b []byte) error {
34+
type t MockItem
2835
return json.Unmarshal(b, (*t)(m))
2936
}
3037

31-
func MockExecutor(m *Mock) *Executor {
32-
if m == nil {
33-
m = &Mock{}
38+
type MockQueue struct {
39+
Q []*MockItem
40+
}
41+
42+
type MockCall MockItem
43+
44+
func (mc *MockCall) MarshalJSON() ([]byte, error) {
45+
item := (*MockItem)(mc)
46+
q := MockQueue{
47+
Q: []*MockItem{item},
3448
}
49+
return json.Marshal(q)
50+
}
3551

36-
path, ctxFunc := mockCommandCtxFunc(m)
52+
func (mc *MockCall) UnmarshalJSON(b []byte) error {
53+
q := MockQueue{}
54+
err := json.Unmarshal(b, &q)
55+
if err != nil {
56+
return err
57+
}
58+
59+
mc = (*MockCall)(q.Q[0])
60+
return nil
61+
}
62+
63+
func (mc *MockCall) NextMockItem() *MockItem {
64+
return (*MockItem)(mc)
65+
}
66+
67+
func (mc *MockQueue) NextMockItem() *MockItem {
68+
if len(mc.Q) == 0 {
69+
return &MockItem{
70+
MockError: "no more calls expected",
71+
}
72+
}
73+
74+
var mi *MockItem
75+
mi, mc.Q = mc.Q[0], mc.Q[1:]
76+
77+
return mi
78+
}
79+
80+
func MockExecutor(md MockItemDispenser) *Executor {
81+
if md == nil {
82+
md = &MockCall{
83+
MockError: "no mocks provided",
84+
}
85+
}
86+
87+
path, ctxFunc := mockCommandCtxFunc(md)
3788
executor := NewExecutor(context.Background(), path)
3889
executor.cmdCtxFunc = ctxFunc
3990
return executor
4091
}
4192

42-
func mockCommandCtxFunc(e *Mock) (string, cmdCtxFunc) {
93+
func mockCommandCtxFunc(md MockItemDispenser) (string, cmdCtxFunc) {
4394
return os.Args[0], func(ctx context.Context, path string, arg ...string) *exec.Cmd {
4495
cmd := exec.CommandContext(ctx, os.Args[0], os.Args[1:]...)
4596

46-
expectedJson, _ := e.MarshalJSON()
97+
b, err := md.NextMockItem().MarshalJSON()
98+
if err != nil {
99+
panic(err)
100+
}
101+
expectedJson := string(b)
47102
cmd.Env = []string{"TF_LS_MOCK=" + string(expectedJson)}
48103

49104
return cmd
50105
}
51106
}
52107

53-
func ExecuteMock(rawMockData string) int {
54-
e := &Mock{}
55-
err := e.UnmarshalJSON([]byte(rawMockData))
108+
func ExecuteMockData(rawMockData string) int {
109+
mi := &MockItem{}
110+
err := mi.UnmarshalJSON([]byte(rawMockData))
56111
if err != nil {
57-
fmt.Fprint(os.Stderr, "unable to unmarshal mock response")
112+
fmt.Fprintf(os.Stderr, "unable to unmarshal mock response: %s", err)
113+
return 1
114+
}
115+
return validateMockItem(mi, os.Args[1:], os.Stdout, os.Stderr)
116+
}
117+
118+
func validateMockItem(m *MockItem, args []string, stdout, stderr io.Writer) int {
119+
if m.MockError != "" {
120+
fmt.Fprintf(stderr, m.MockError)
58121
return 1
59122
}
60123

61-
givenArgs := os.Args[1:]
62-
if !reflect.DeepEqual(e.Args, givenArgs) {
63-
fmt.Fprintf(os.Stderr, "arguments don't match.\nexpected: %q\ngiven: %q\n",
64-
e.Args, givenArgs)
124+
givenArgs := args
125+
if !reflect.DeepEqual(m.Args, givenArgs) {
126+
fmt.Fprintf(stderr,
127+
"arguments don't match.\nexpected: %q\ngiven: %q\n",
128+
m.Args, givenArgs)
65129
return 1
66130
}
67131

68-
if e.SleepDuration > 0 {
69-
time.Sleep(e.SleepDuration)
132+
if m.SleepDuration > 0 {
133+
time.Sleep(m.SleepDuration)
70134
}
71135

72-
fmt.Fprint(os.Stdout, e.Stdout)
73-
fmt.Fprint(os.Stderr, e.Stderr)
136+
fmt.Fprint(stdout, m.Stdout)
137+
fmt.Fprint(stderr, m.Stderr)
74138

75-
return e.ExitCode
139+
return m.ExitCode
76140
}

internal/terraform/exec/exec_mock_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77

88
func TestMain(m *testing.M) {
99
if v := os.Getenv("TF_LS_MOCK"); v != "" {
10-
os.Exit(ExecuteMock(v))
10+
os.Exit(ExecuteMockData(v))
1111
return
1212
}
1313

internal/terraform/exec/exec_test.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import (
77
)
88

99
func TestExec_timeout(t *testing.T) {
10-
e := MockExecutor(&Mock{
10+
e := MockExecutor(&MockCall{
1111
Args: []string{"version"},
1212
SleepDuration: 100 * time.Millisecond,
1313
Stdout: "Terraform v0.12.0\n",
1414
})
15+
e.SetWorkdir("/tmp")
1516
e.timeout = 1 * time.Millisecond
1617

1718
expectedErr := ExecTimeoutError([]string{"terraform", "version"}, e.timeout)
@@ -30,11 +31,12 @@ func TestExec_timeout(t *testing.T) {
3031
}
3132

3233
func TestExec_Version(t *testing.T) {
33-
e := MockExecutor(&Mock{
34+
e := MockExecutor(&MockCall{
3435
Args: []string{"version"},
3536
Stdout: "Terraform v0.12.0\n",
3637
ExitCode: 0,
3738
})
39+
e.SetWorkdir("/tmp")
3840
v, err := e.Version()
3941
if err != nil {
4042
t.Fatal(err)
@@ -45,11 +47,12 @@ func TestExec_Version(t *testing.T) {
4547
}
4648

4749
func TestExec_ProviderSchemas(t *testing.T) {
48-
e := MockExecutor(&Mock{
50+
e := MockExecutor(&MockCall{
4951
Args: []string{"providers", "schema", "-json"},
5052
Stdout: `{"format_version": "0.1"}`,
5153
ExitCode: 0,
5254
})
55+
e.SetWorkdir("/tmp")
5356

5457
ps, err := e.ProviderSchemas()
5558
if err != nil {

internal/terraform/lang/config_block.go

+12-2
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import (
99
hcl "github.com/hashicorp/hcl/v2"
1010
"github.com/hashicorp/hcl/v2/hclsyntax"
1111
tfjson "github.com/hashicorp/terraform-json"
12+
"github.com/hashicorp/terraform-ls/internal/terraform/schema"
1213
lsp "github.com/sourcegraph/go-lsp"
1314
)
1415

1516
type ConfigBlock interface {
1617
CompletionItemsAtPos(pos hcl.Pos) (lsp.CompletionList, error)
17-
LoadSchema(ps *tfjson.ProviderSchemas) error
1818
Name() string
1919
BlockType() string
2020
}
@@ -27,12 +27,15 @@ type configBlockFactory interface {
2727
type Parser interface {
2828
SetLogger(*log.Logger)
2929
SetCapabilities(lsp.TextDocumentClientCapabilities)
30+
SetSchemaReader(schema.Reader)
3031
ParseBlockFromHCL(*hcl.Block) (ConfigBlock, error)
3132
}
3233

3334
type parser struct {
3435
logger *log.Logger
3536
caps lsp.TextDocumentClientCapabilities
37+
38+
schemaReader schema.Reader
3639
}
3740

3841
func FindCompatibleParser(v string) (Parser, error) {
@@ -66,9 +69,16 @@ func (p *parser) SetCapabilities(caps lsp.TextDocumentClientCapabilities) {
6669
p.caps = caps
6770
}
6871

72+
func (p *parser) SetSchemaReader(sr schema.Reader) {
73+
p.schemaReader = sr
74+
}
75+
6976
func (p *parser) blockTypes() map[string]configBlockFactory {
7077
return map[string]configBlockFactory{
71-
"provider": &providerBlockFactory{logger: p.logger},
78+
"provider": &providerBlockFactory{
79+
logger: p.logger,
80+
schemaReader: p.schemaReader,
81+
},
7282
// "resource": ResourceBlock,
7383
// "data": ResourceBlock,
7484
// "variable": VariableBlock,

internal/terraform/lang/errors.go

-9
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,3 @@ func (e *emptyConfigErr) Error() string {
2121
func EmptyConfigErr() *emptyConfigErr {
2222
return &emptyConfigErr{}
2323
}
24-
25-
type SchemaUnavailableErr struct {
26-
BlockType string
27-
FullName string
28-
}
29-
30-
func (e *SchemaUnavailableErr) Error() string {
31-
return fmt.Sprintf("schema unavailable for %s %q", e.BlockType, e.FullName)
32-
}

0 commit comments

Comments
 (0)