Skip to content
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

settings: Make root modules configurable #198

Merged
merged 1 commit into from
Jul 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, make ctx first argument

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't disagree, but I think I'd prefer to change all of the With* functions at the same time and perhaps do that in a separate PR, if that's ok and follow the same pattern throughout the package for now?

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