diff --git a/docs/SETTINGS.md b/docs/SETTINGS.md index cb88baa0d..7a599f214 100644 --- a/docs/SETTINGS.md +++ b/docs/SETTINGS.md @@ -4,6 +4,13 @@ The language server supports the following configuration options: +## `terraformExecPath` (`string`) + +Path to the Terraform binary. + +This is usually looked up automatically from `$PATH` and should not need to be +specified in majority of cases. Use this to override the automatic lookup. + ## `rootModulePaths` (`[]string`) This allows overriding automatic root module discovery by passing a static list diff --git a/internal/cmd/serve_command.go b/internal/cmd/serve_command.go index 031041fd2..ced87a111 100644 --- a/internal/cmd/serve_command.go +++ b/internal/cmd/serve_command.go @@ -42,7 +42,7 @@ func (c *ServeCommand) flags() *flag.FlagSet { fs.IntVar(&c.port, "port", 0, "port number to listen on (turns server into TCP mode)") fs.StringVar(&c.logFilePath, "log-file", "", "path to a file to log into with support "+ "for variables (e.g. Timestamp, Pid, Ppid) via Go template syntax {{.VarName}}") - fs.StringVar(&c.tfExecPath, "tf-exec", "", "path to Terraform binary") + fs.StringVar(&c.tfExecPath, "tf-exec", "", "(DEPRECATED) path to Terraform binary. Use terraformExecPath LSP config option instead.") fs.StringVar(&c.tfExecTimeout, "tf-exec-timeout", "", "Overrides Terraform execution timeout (e.g. 30s)") fs.StringVar(&c.tfExecLogPath, "tf-log-file", "", "path to a file for Terraform executions"+ " to be logged into with support for variables (e.g. Timestamp, Pid, Ppid) via Go template"+ @@ -120,6 +120,9 @@ func (c *ServeCommand) Run(args []string) int { logger.Printf("Terraform execution timeout set to %s", d) } + // Setting this option as a CLI flag is deprecated + // in favor of `terraformExecPath` LSP config option. + // This validation code is duplicated, make changes accordingly. if c.tfExecPath != "" { path := c.tfExecPath @@ -140,6 +143,7 @@ func (c *ServeCommand) Run(args []string) int { ctx = lsctx.WithTerraformExecPath(ctx, path) logger.Printf("Terraform exec path set to %q", path) + logger.Println("[WARN] -tf-exec is deprecated in favor of `terraformExecPath` LSP config option") } if c.reqConcurrency != 0 { diff --git a/internal/langserver/handlers/formatting.go b/internal/langserver/handlers/formatting.go index 6a3515c1d..7f88f926a 100644 --- a/internal/langserver/handlers/formatting.go +++ b/internal/langserver/handlers/formatting.go @@ -36,6 +36,8 @@ func (h *logHandler) TextDocumentFormatting(ctx context.Context, params lsp.Docu return edits, err } + h.logger.Printf("formatting document via %q", tfExec.GetExecPath()) + formatted, err := tfExec.Format(ctx, original) if err != nil { return edits, err diff --git a/internal/langserver/handlers/initialize.go b/internal/langserver/handlers/initialize.go index 321e5c3ec..8fd7d8f81 100644 --- a/internal/langserver/handlers/initialize.go +++ b/internal/langserver/handlers/initialize.go @@ -14,7 +14,7 @@ import ( "github.com/mitchellh/go-homedir" ) -func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParams) (lsp.InitializeResult, error) { +func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams) (lsp.InitializeResult, error) { serverCaps := lsp.InitializeResult{ Capabilities: lsp.ServerCapabilities{ TextDocumentSync: lsp.TextDocumentSyncOptions{ @@ -83,16 +83,16 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam return serverCaps, err } - w, err := lsctx.Watcher(ctx) + out, err := settings.DecodeOptions(params.InitializationOptions) if err != nil { return serverCaps, err } - - out, err := settings.DecodeOptions(params.InitializationOptions) + err = out.Options.Validate() if err != nil { return serverCaps, err } - err = out.Options.Validate() + + err = svc.configureSessionDependencies(out.Options) if err != nil { return serverCaps, err } @@ -140,24 +140,18 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam }) } - walker, err := lsctx.ModuleWalker(ctx) - if err != nil { - return serverCaps, err - } - walker.SetLogger(lh.logger) - var excludeModulePaths []string for _, rawPath := range cfgOpts.ExcludeModulePaths { modPath, err := resolvePath(rootDir, rawPath) if err != nil { - lh.logger.Printf("Ignoring excluded module path %s: %s", rawPath, err) + svc.logger.Printf("Ignoring excluded module path %s: %s", rawPath, err) continue } excludeModulePaths = append(excludeModulePaths, modPath) } - walker.SetExcludeModulePaths(excludeModulePaths) - walker.EnqueuePath(fh.Dir()) + svc.walker.SetExcludeModulePaths(excludeModulePaths) + svc.walker.EnqueuePath(fh.Dir()) // Walker runs asynchronously so we're intentionally *not* // passing the request context here @@ -165,7 +159,7 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam // Walker is also started early to allow gradual consumption // and avoid overfilling the queue - err = walker.StartWalking(walkerCtx) + err = svc.walker.StartWalking(walkerCtx) if err != nil { return serverCaps, err } @@ -182,13 +176,13 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam continue } - walker.EnqueuePath(modPath) + svc.walker.EnqueuePath(modPath) } } // Static user-provided paths take precedence over dynamic discovery if len(cfgOpts.ModulePaths) > 0 { - lh.logger.Printf("Attempting to add %d static module paths", len(cfgOpts.ModulePaths)) + svc.logger.Printf("Attempting to add %d static module paths", len(cfgOpts.ModulePaths)) for _, rawPath := range cfgOpts.ModulePaths { modPath, err := resolvePath(rootDir, rawPath) if err != nil { @@ -199,7 +193,7 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam continue } - err = w.AddModule(modPath) + err = svc.watcher.AddModule(modPath) if err != nil { return serverCaps, err } diff --git a/internal/langserver/handlers/service.go b/internal/langserver/handlers/service.go index 34804cdb4..1f69d06cc 100644 --- a/internal/langserver/handlers/service.go +++ b/internal/langserver/handlers/service.go @@ -46,6 +46,7 @@ type service struct { newWalker module.WalkerFactory tfDiscoFunc discovery.DiscoveryFunc tfExecFactory exec.ExecutorFactory + tfExecOpts *exec.ExecutorOpts additionalHandlers map[string]rpch.Func } @@ -92,53 +93,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { lh := LogHandler(svc.logger) cc := &lsp.ClientCapabilities{} - // The following is set via CLI flags, hence available in the server context - execOpts := &exec.ExecutorOpts{} - if path, ok := lsctx.TerraformExecPath(svc.srvCtx); ok { - execOpts.ExecPath = path - } else { - tfExecPath, err := svc.tfDiscoFunc() - if err == nil { - execOpts.ExecPath = tfExecPath - } - } - if path, ok := lsctx.TerraformExecLogPath(svc.srvCtx); ok { - execOpts.ExecLogPath = path - } - if timeout, ok := lsctx.TerraformExecTimeout(svc.srvCtx); ok { - execOpts.Timeout = timeout - } - - svc.sessCtx = exec.WithExecutorOpts(svc.sessCtx, execOpts) - svc.sessCtx = exec.WithExecutorFactory(svc.sessCtx, svc.tfExecFactory) - - store, err := state.NewStateStore() - if err != nil { - return nil, err - } - store.SetLogger(svc.logger) - - err = schemas.PreloadSchemasToStore(store.ProviderSchemas) - if err != nil { - return nil, err - } - - svc.modMgr = svc.newModuleManager(svc.sessCtx, svc.fs, store.Modules, store.ProviderSchemas) - svc.modMgr.SetLogger(svc.logger) - - svc.walker = svc.newWalker(svc.fs, svc.modMgr) - - ww, err := svc.newWatcher(svc.fs, svc.modMgr) - if err != nil { - return nil, err - } - svc.watcher = ww - svc.watcher.SetLogger(svc.logger) - err = svc.watcher.Start() - if err != nil { - return nil, err - } - notifier := diagnostics.NewNotifier(svc.sessCtx, svc.logger) rootDir := "" @@ -152,10 +106,8 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { if err != nil { return nil, err } - ctx = lsctx.WithDocumentStorage(ctx, svc.fs) + ctx = lsctx.WithClientCapabilitiesSetter(ctx, cc) - ctx = lsctx.WithWatcher(ctx, ww) - ctx = lsctx.WithModuleWalker(ctx, svc.walker) ctx = lsctx.WithRootDirectory(ctx, &rootDir) ctx = lsctx.WithCommandPrefix(ctx, &commandPrefix) ctx = lsctx.WithClientName(ctx, &clientName) @@ -166,7 +118,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { ctx = lsctx.WithLanguageServerVersion(ctx, version) } - return handle(ctx, req, lh.Initialize) + return handle(ctx, req, svc.Initialize) }, "initialized": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { err := session.ConfirmInitialization(req) @@ -194,7 +146,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { ctx = lsctx.WithDiagnosticsNotifier(ctx, notifier) ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = lsctx.WithModuleManager(ctx, svc.modMgr) - ctx = lsctx.WithWatcher(ctx, ww) + ctx = lsctx.WithWatcher(ctx, svc.watcher) return handle(ctx, req, lh.TextDocumentDidOpen) }, "textDocument/didClose": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { @@ -298,7 +250,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { } ctx = lsctx.WithDocumentStorage(ctx, svc.fs) - ctx = exec.WithExecutorOpts(ctx, execOpts) + ctx = exec.WithExecutorOpts(ctx, svc.tfExecOpts) ctx = exec.WithExecutorFactory(ctx, svc.tfExecFactory) return handle(ctx, req, lh.TextDocumentFormatting) @@ -324,7 +276,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { ctx = lsctx.WithDiagnosticsNotifier(ctx, notifier) ctx = lsctx.WithExperimentalFeatures(ctx, &expFeatures) ctx = lsctx.WithModuleFinder(ctx, svc.modMgr) - ctx = exec.WithExecutorOpts(ctx, execOpts) + ctx = exec.WithExecutorOpts(ctx, svc.tfExecOpts) return handle(ctx, req, lh.TextDocumentDidSave) }, @@ -360,10 +312,10 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { ctx = lsctx.WithModuleManager(ctx, svc.modMgr) ctx = lsctx.WithModuleFinder(ctx, svc.modMgr) ctx = lsctx.WithModuleWalker(ctx, svc.walker) - ctx = lsctx.WithWatcher(ctx, ww) + ctx = lsctx.WithWatcher(ctx, svc.watcher) ctx = lsctx.WithRootDirectory(ctx, &rootDir) ctx = lsctx.WithDiagnosticsNotifier(ctx, notifier) - ctx = exec.WithExecutorOpts(ctx, execOpts) + ctx = exec.WithExecutorOpts(ctx, svc.tfExecOpts) ctx = exec.WithExecutorFactory(ctx, svc.tfExecFactory) return handle(ctx, req, lh.WorkspaceExecuteCommand) @@ -419,6 +371,69 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return convertMap(m), nil } +func (svc *service) configureSessionDependencies(cfgOpts *settings.Options) error { + // The following is set via CLI flags, hence available in the server context + execOpts := &exec.ExecutorOpts{} + cliExecPath, ok := lsctx.TerraformExecPath(svc.srvCtx) + if ok { + if len(cfgOpts.TerraformExecPath) > 0 { + return fmt.Errorf("Terraform exec path can either be set via (-tf-exec) CLI flag " + + "or (terraformExecPath) LSP config option, not both") + } + execOpts.ExecPath = cliExecPath + } else if len(cfgOpts.TerraformExecPath) > 0 { + execOpts.ExecPath = cfgOpts.TerraformExecPath + } else { + path, err := svc.tfDiscoFunc() + if err == nil { + execOpts.ExecPath = path + } + } + svc.srvCtx = lsctx.WithTerraformExecPath(svc.srvCtx, execOpts.ExecPath) + + if path, ok := lsctx.TerraformExecLogPath(svc.srvCtx); ok { + execOpts.ExecLogPath = path + } + if timeout, ok := lsctx.TerraformExecTimeout(svc.srvCtx); ok { + execOpts.Timeout = timeout + } + + svc.tfExecOpts = execOpts + + svc.sessCtx = exec.WithExecutorOpts(svc.sessCtx, execOpts) + svc.sessCtx = exec.WithExecutorFactory(svc.sessCtx, svc.tfExecFactory) + + store, err := state.NewStateStore() + if err != nil { + return err + } + store.SetLogger(svc.logger) + + err = schemas.PreloadSchemasToStore(store.ProviderSchemas) + if err != nil { + return err + } + + svc.modMgr = svc.newModuleManager(svc.sessCtx, svc.fs, store.Modules, store.ProviderSchemas) + svc.modMgr.SetLogger(svc.logger) + + svc.walker = svc.newWalker(svc.fs, svc.modMgr) + svc.walker.SetLogger(svc.logger) + + ww, err := svc.newWatcher(svc.fs, svc.modMgr) + if err != nil { + return err + } + svc.watcher = ww + svc.watcher.SetLogger(svc.logger) + err = svc.watcher.Start() + if err != nil { + return err + } + + return nil +} + func (svc *service) Finish(_ jrpc2.Assigner, status jrpc2.ServerStatus) { if status.Closed || status.Err != nil { svc.logger.Printf("session stopped unexpectedly (err: %v)", status.Err) diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 14f8f0c33..835c0428e 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -2,6 +2,8 @@ package settings import ( "fmt" + "os" + "path/filepath" "github.com/mitchellh/mapstructure" ) @@ -20,7 +22,7 @@ type Options struct { ExperimentalFeatures ExperimentalFeatures `mapstructure:"experimentalFeatures"` // TODO: Need to check for conflict with CLI flags - // TerraformExecPath string + TerraformExecPath string `mapstructure:"terraformExecPath"` // TerraformExecTimeout time.Duration // TerraformLogFilePath string } @@ -29,6 +31,21 @@ func (o *Options) Validate() error { if len(o.ModulePaths) != 0 && len(o.ExcludeModulePaths) != 0 { return fmt.Errorf("at most one of `rootModulePaths` and `excludeModulePaths` could be set") } + + if o.TerraformExecPath != "" { + path := o.TerraformExecPath + if !filepath.IsAbs(path) { + return fmt.Errorf("Expected absolute path for Terraform binary, got %q", path) + } + stat, err := os.Stat(path) + if err != nil { + return fmt.Errorf("Unable to find Terraform binary: %s", err) + } + if stat.IsDir() { + return fmt.Errorf("Expected a Terraform binary, got a directory: %q", path) + } + } + return nil } diff --git a/internal/terraform/exec/exec.go b/internal/terraform/exec/exec.go index bffbe47a4..51d7717bb 100644 --- a/internal/terraform/exec/exec.go +++ b/internal/terraform/exec/exec.go @@ -136,8 +136,11 @@ func (e *Executor) Validate(ctx context.Context) ([]tfjson.Diagnostic, error) { } validation, err := e.tf.Validate(ctx) + if err != nil { + return []tfjson.Diagnostic{}, e.contextfulError(ctx, "Validate", err) + } - return validation.Diagnostics, e.contextfulError(ctx, "Validate", err) + return validation.Diagnostics, nil } func (e *Executor) Version(ctx context.Context) (*version.Version, map[string]*version.Version, error) {