Skip to content

Commit

Permalink
settings: Make root modules configurable (#198)
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko authored Jul 10, 2020
1 parent 84dbb17 commit aaf66a9
Show file tree
Hide file tree
Showing 20 changed files with 2,037 additions and 1 deletion.
44 changes: 44 additions & 0 deletions docs/SETTINGS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Settings

## Supported Options

The language server supports the following configuration options:

## `rootModulePaths` (`[]string`)

This allows overriding automatic root module discovery by passing a static list
of absolute paths to root modules (i.e. folders with `*.tf` files
which have been `terraform init`-ed).

## How to pass settings

The server expects static settings to be passed as part of LSP `initialize` call,
but how settings are requested from on the UI side depends on the client.

### Sublime Text

Use `initializationOptions` key under the `clients.terraform` section, e.g.

```json
{
"clients": {
"terraform": {
"initializationOptions": {
"rootModulePaths": ["/any/path"]
},
}
}
}
```

### VS Code

Use `terraform-ls`, e.g.

```json
{
"terraform-ls": {
"rootModulePaths": ["/any/path"]
}
}
```
5 changes: 4 additions & 1 deletion docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ in the [README.md](../README.md) if that is applicable to your client
(i.e. if the client doesn't download the server itself).

Instructions for popular IDEs are below and pull requests
for updates or addition of more IDEs are welcomed.
for updates or addition of more IDEs are welcomed.

See also [settings](./SETTINGS.md) to understand
how you may configure the settings.

## Emacs

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/hashicorp/terraform-json v0.5.0
github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5
github.com/mitchellh/cli v1.0.0
github.com/mitchellh/mapstructure v1.3.2
github.com/pmezard/go-difflib v1.0.0
github.com/sourcegraph/go-lsp v0.0.0-20200117082640-b19bb38222e2
github.com/zclconf/go-cty v1.2.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg=
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w=
Expand Down
13 changes: 13 additions & 0 deletions internal/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var (
ctxTfFormatterFinder = &contextKey{"terraform formatter finder"}
ctxRootModuleCaFi = &contextKey{"root module candidate finder"}
ctxRootModuleWalker = &contextKey{"root module walker"}
ctxRootModuleLoader = &contextKey{"root module loader"}
ctxRootDir = &contextKey{"root directory"}
)

Expand Down Expand Up @@ -198,3 +199,15 @@ func RootModuleWalker(ctx context.Context) (*rootmodule.Walker, error) {
}
return w, nil
}

func WithRootModuleLoader(rml rootmodule.RootModuleLoader, ctx context.Context) context.Context {
return context.WithValue(ctx, ctxRootModuleLoader, rml)
}

func RootModuleLoader(ctx context.Context) (rootmodule.RootModuleLoader, error) {
w, ok := ctx.Value(ctxRootModuleLoader).(rootmodule.RootModuleLoader)
if !ok {
return nil, missingContextErr(ctxRootModuleLoader)
}
return w, nil
}
59 changes: 59 additions & 0 deletions internal/settings/settings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package settings

import (
"fmt"
"path/filepath"

"github.com/hashicorp/go-multierror"
"github.com/mitchellh/mapstructure"
)

type Options struct {
// RootModulePaths describes a list of absolute paths to root modules
RootModulePaths []string `mapstructure:"rootModulePaths"`

// TODO: Need to check for conflict with CLI flags
// TerraformExecPath string
// TerraformExecTimeout time.Duration
// TerraformLogFilePath string
}

func (o *Options) Validate() error {
var result *multierror.Error

for _, p := range o.RootModulePaths {
if !filepath.IsAbs(p) {
result = multierror.Append(result, fmt.Errorf("%q is not an absolute path", p))
}
}

return result.ErrorOrNil()
}

type DecodedOptions struct {
Options *Options
UnusedKeys []string
}

func DecodeOptions(input interface{}) (*DecodedOptions, error) {
var md mapstructure.Metadata
var options Options

config := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &options,
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
panic(err)
}

if err := decoder.Decode(input); err != nil {
return nil, err
}

return &DecodedOptions{
Options: &options,
UnusedKeys: md.Unused,
}, nil
}
54 changes: 54 additions & 0 deletions internal/settings/settings_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package settings

import (
"testing"

"github.com/google/go-cmp/cmp"
)

func TestDecodeOptions_nil(t *testing.T) {
out, err := DecodeOptions(nil)
if err != nil {
t.Fatal(err)
}
opts := out.Options

if opts.RootModulePaths != nil {
t.Fatalf("expected no options for nil, %#v given", opts.RootModulePaths)
}
}

func TestDecodeOptions_wrongType(t *testing.T) {
_, err := DecodeOptions(map[string]interface{}{
"rootModulePaths": "/random/path",
})
if err == nil {
t.Fatal("expected decoding of wrong type to result in error")
}
}

func TestDecodeOptions_success(t *testing.T) {
out, err := DecodeOptions(map[string]interface{}{
"rootModulePaths": []string{"/random/path"},
})
if err != nil {
t.Fatal(err)
}
opts := out.Options
expectedPaths := []string{"/random/path"}
if diff := cmp.Diff(expectedPaths, opts.RootModulePaths); diff != "" {
t.Fatalf("options mismatch: %s", diff)
}
}

func TestDecodedOptions_Validate(t *testing.T) {
opts := &Options{
RootModulePaths: []string{
"./relative/path",
},
}
err := opts.Validate()
if err == nil {
t.Fatal("expected relative path to fail validation")
}
}
11 changes: 11 additions & 0 deletions internal/terraform/rootmodule/root_module_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,14 @@ func (rmm *rootModuleManager) PathsToWatch() []string {
}
return paths
}

// NewRootModuleLoader allows adding & loading root modules
// with a given context. This can be passed down to any handler
// which itself will have short-lived context
// therefore couldn't finish loading the root module asynchronously
// after it responds to the client
func NewRootModuleLoader(ctx context.Context, rmm RootModuleManager) RootModuleLoader {
return func(dir string) (RootModule, error) {
return rmm.AddAndStartLoadingRootModule(ctx, dir)
}
}
2 changes: 2 additions & 0 deletions internal/terraform/rootmodule/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type RootModuleCandidateFinder interface {
RootModuleCandidatesByPath(path string) RootModules
}

type RootModuleLoader func(dir string) (RootModule, error)

type RootModuleManager interface {
ParserFinder
TerraformFormatterFinder
Expand Down
43 changes: 43 additions & 0 deletions langserver/handlers/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"context"
"fmt"

"github.com/creachadair/jrpc2"
lsctx "github.com/hashicorp/terraform-ls/internal/context"
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
"github.com/hashicorp/terraform-ls/internal/settings"
lsp "github.com/sourcegraph/go-lsp"
)

Expand Down Expand Up @@ -49,11 +51,52 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam
return serverCaps, err
}

addAndLoadRootModule, err := lsctx.RootModuleLoader(ctx)
if err != nil {
return serverCaps, err
}

w, err := lsctx.Watcher(ctx)
if err != nil {
return serverCaps, err
}

out, err := settings.DecodeOptions(params.InitializationOptions)
if err != nil {
return serverCaps, err
}
err = out.Options.Validate()
if err != nil {
return serverCaps, err
}
if len(out.UnusedKeys) > 0 {
jrpc2.ServerPush(ctx, "window/showMessage", &lsp.ShowMessageParams{
Type: lsp.MTWarning,
Message: fmt.Sprintf("Unknown configuration options: %q", out.UnusedKeys),
})
}
cfgOpts := out.Options

// Static user-provided paths take precedence over dynamic discovery
if len(cfgOpts.RootModulePaths) > 0 {
lh.logger.Printf("Attempting to add %d static root module paths", len(cfgOpts.RootModulePaths))
for _, rmPath := range cfgOpts.RootModulePaths {
rm, err := addAndLoadRootModule(rmPath)
if err != nil {
return serverCaps, err
}

paths := rm.PathsToWatch()
lh.logger.Printf("Adding %d paths of root module for watching (%s)", len(paths), rmPath)
err = w.AddPaths(paths)
if err != nil {
return serverCaps, err
}
}

return serverCaps, nil
}

walker, err := lsctx.RootModuleWalker(ctx)
if err != nil {
return serverCaps, err
Expand Down
3 changes: 3 additions & 0 deletions langserver/handlers/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) {
return nil, err
}

rmLoader := rootmodule.NewRootModuleLoader(svc.sessCtx, svc.modMgr)

rootDir := ""

m := map[string]rpch.Func{
Expand All @@ -135,6 +137,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) {
ctx = lsctx.WithRootModuleWalker(svc.walker, ctx)
ctx = lsctx.WithRootDirectory(&rootDir, ctx)
ctx = lsctx.WithRootModuleManager(svc.modMgr, ctx)
ctx = lsctx.WithRootModuleLoader(rmLoader, ctx)

return handle(ctx, req, lh.Initialize)
},
Expand Down
9 changes: 9 additions & 0 deletions vendor/github.com/mitchellh/mapstructure/.travis.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 57 additions & 0 deletions vendor/github.com/mitchellh/mapstructure/CHANGELOG.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit aaf66a9

Please sign in to comment.