diff --git a/docs/commands.md b/docs/commands.md index 349777d77..791738169 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -183,3 +183,25 @@ installed version. } } ``` + +### `module.terraform` + +Provides information about the terraform binary version for the current module. + +**Arguments:** + + - `uri` - URI of the directory of the module in question, e.g. `file:///path/to/network` + +**Outputs:** + + - `v` - describes version of the format; Will be used in the future to communicate format changes. + - `required_version` - Version constraint specified in configuration + - `discovered_version` - Version discovered from `terraform version --json` in the directory specified in `uri` + +```json +{ + "v": 0, + "required_version": "~> 0.15", + "discovered_version": "1.1.0" +} +``` diff --git a/internal/langserver/handlers/command/terraform.go b/internal/langserver/handlers/command/terraform.go new file mode 100644 index 000000000..95ccd17ca --- /dev/null +++ b/internal/langserver/handlers/command/terraform.go @@ -0,0 +1,62 @@ +package command + +import ( + "context" + "fmt" + + "github.com/creachadair/jrpc2/code" + "github.com/hashicorp/terraform-ls/internal/langserver/cmd" + "github.com/hashicorp/terraform-ls/internal/langserver/progress" + "github.com/hashicorp/terraform-ls/internal/uri" +) + +const terraformVersionRequestVersion = 0 + +type terraformInfoResponse struct { + FormatVersion int `json:"v"` + RequiredVersion string `json:"required_version,omitempty"` + DiscoveredVersion string `json:"discovered_version,omitempty"` +} + +func (h *CmdHandler) TerraformVersionRequestHandler(ctx context.Context, args cmd.CommandArgs) (interface{}, error) { + progress.Begin(ctx, "Initializing") + defer func() { + progress.End(ctx, "Finished") + }() + + response := terraformInfoResponse{ + FormatVersion: terraformVersionRequestVersion, + } + + progress.Report(ctx, "Finding current module info ...") + modUri, ok := args.GetString("uri") + if !ok || modUri == "" { + return response, fmt.Errorf("%w: expected module uri argument to be set", code.InvalidParams.Err()) + } + + if !uri.IsURIValid(modUri) { + return response, fmt.Errorf("URI %q is not valid", modUri) + } + + modPath, err := uri.PathFromURI(modUri) + if err != nil { + return response, err + } + + mod, _ := h.StateStore.Modules.ModuleByPath(modPath) + if mod == nil { + return response, nil + } + + progress.Report(ctx, "Recording terraform version info ...") + if mod.TerraformVersion != nil { + response.DiscoveredVersion = mod.TerraformVersion.String() + } + if mod.Meta.CoreRequirements != nil { + response.RequiredVersion = mod.Meta.CoreRequirements.String() + } + + progress.Report(ctx, "Sending response ...") + + return response, nil +} diff --git a/internal/langserver/handlers/execute_command.go b/internal/langserver/handlers/execute_command.go index 8164f4d96..a968246de 100644 --- a/internal/langserver/handlers/execute_command.go +++ b/internal/langserver/handlers/execute_command.go @@ -24,6 +24,7 @@ func cmdHandlers(svc *service) cmd.Handlers { cmd.Name("terraform.validate"): cmdHandler.TerraformValidateHandler, cmd.Name("module.calls"): cmdHandler.ModuleCallsHandler, cmd.Name("module.providers"): cmdHandler.ModuleProvidersHandler, + cmd.Name("module.terraform"): cmdHandler.TerraformVersionRequestHandler, } } diff --git a/internal/langserver/handlers/execute_command_terraform_version_test.go b/internal/langserver/handlers/execute_command_terraform_version_test.go new file mode 100644 index 000000000..632c6edfe --- /dev/null +++ b/internal/langserver/handlers/execute_command_terraform_version_test.go @@ -0,0 +1,95 @@ +package handlers + +import ( + "fmt" + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-ls/internal/document" + "github.com/hashicorp/terraform-ls/internal/langserver" + "github.com/hashicorp/terraform-ls/internal/langserver/cmd" + "github.com/hashicorp/terraform-ls/internal/state" + "github.com/hashicorp/terraform-ls/internal/terraform/exec" + "github.com/hashicorp/terraform-ls/internal/uri" + "github.com/hashicorp/terraform-ls/internal/walker" + tfaddr "github.com/hashicorp/terraform-registry-address" + tfmod "github.com/hashicorp/terraform-schema/module" + "github.com/stretchr/testify/mock" +) + +func TestLangServer_workspaceExecuteCommand_terraformVersion_basic(t *testing.T) { + modDir := t.TempDir() + modUri := uri.FromPath(modDir) + + s, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + + err = s.Modules.Add(modDir) + if err != nil { + t.Fatal(err) + } + + metadata := &tfmod.Meta{ + Path: modDir, + CoreRequirements: testConstraint(t, "~> 0.15"), + } + + err = s.Modules.UpdateMetadata(modDir, metadata, nil) + if err != nil { + t.Fatal(err) + } + + ver, err := version.NewVersion("1.1.0") + if err != nil { + t.Fatal(err) + } + + err = s.Modules.UpdateTerraformVersion(modDir, ver, map[tfaddr.Provider]*version.Version{}, nil) + if err != nil { + t.Fatal(err) + } + + wc := walker.NewWalkerCollector() + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + TerraformCalls: &exec.TerraformMockCalls{ + PerWorkDir: map[string][]*mock.Call{ + modDir: validTfMockCalls(), + }, + }, + StateStore: s, + WalkerCollector: wc, + })) + stop := ls.Start(t) + defer stop() + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, modUri)}) + waitForWalkerPath(t, s, wc, document.DirHandleFromURI(modUri)) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "workspace/executeCommand", + ReqParams: fmt.Sprintf(`{ + "command": %q, + "arguments": ["uri=%s"] + }`, cmd.Name("module.terraform"), modUri)}, `{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "v": 0, + "required_version": "~\u003e 0.15", + "discovered_version": "1.1.0" + } + }`) +} diff --git a/internal/langserver/handlers/handlers_test.go b/internal/langserver/handlers/handlers_test.go index 2539ce704..db9c9fd6d 100644 --- a/internal/langserver/handlers/handlers_test.go +++ b/internal/langserver/handlers/handlers_test.go @@ -75,7 +75,8 @@ func initializeResponse(t *testing.T, commandPrefix string) string { "experimental": { "referenceCountCodeLens": false, "refreshModuleProviders": false, - "refreshModuleCalls": false + "refreshModuleCalls": false, + "refreshTerraformVersion": false } }, "serverInfo": { diff --git a/internal/langserver/handlers/initialize.go b/internal/langserver/handlers/initialize.go index f20c4f2f8..8e585cf1a 100644 --- a/internal/langserver/handlers/initialize.go +++ b/internal/langserver/handlers/initialize.go @@ -61,6 +61,10 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams) expServerCaps.RefreshModuleCalls = true properties["experimentalCapabilities.refreshModuleCalls"] = true } + if _, ok := expClientCaps.RefreshTerraformVersionCommandId(); ok { + expServerCaps.RefreshTerraformVersion = true + properties["experimentalCapabilities.refreshTerraformVersion"] = true + } serverCaps.Capabilities.Experimental = expServerCaps diff --git a/internal/langserver/handlers/service.go b/internal/langserver/handlers/service.go index d0dccdd8e..64bd34d3d 100644 --- a/internal/langserver/handlers/service.go +++ b/internal/langserver/handlers/service.go @@ -463,6 +463,10 @@ func (svc *service) configureSessionDependencies(ctx context.Context, cfgOpts *s moduleHooks = append(moduleHooks, callRefreshClientCommand(svc.server, commandId)) } + if commandId, ok := lsp.ExperimentalClientCapabilities(cc.Experimental).RefreshTerraformVersionCommandId(); ok { + moduleHooks = append(moduleHooks, callRefreshClientCommand(svc.server, commandId)) + } + if cc.Workspace.SemanticTokens.RefreshSupport { moduleHooks = append(moduleHooks, refreshSemanticTokens(svc.server)) } diff --git a/internal/protocol/experimental.go b/internal/protocol/experimental.go index 07cd12a60..5a596913f 100644 --- a/internal/protocol/experimental.go +++ b/internal/protocol/experimental.go @@ -1,9 +1,10 @@ package protocol type ExperimentalServerCapabilities struct { - ReferenceCountCodeLens bool `json:"referenceCountCodeLens"` - RefreshModuleProviders bool `json:"refreshModuleProviders"` - RefreshModuleCalls bool `json:"refreshModuleCalls"` + ReferenceCountCodeLens bool `json:"referenceCountCodeLens"` + RefreshModuleProviders bool `json:"refreshModuleProviders"` + RefreshModuleCalls bool `json:"refreshModuleCalls"` + RefreshTerraformVersion bool `json:"refreshTerraformVersion"` } type ExpClientCapabilities map[string]interface{} @@ -42,6 +43,15 @@ func (cc ExpClientCapabilities) RefreshModuleCallsCommandId() (string, bool) { return cmdId, ok } +func (cc ExpClientCapabilities) RefreshTerraformVersionCommandId() (string, bool) { + if cc == nil { + return "", false + } + + cmdId, ok := cc["refreshTerraformVersionCommandId"].(string) + return cmdId, ok +} + func (cc ExpClientCapabilities) TelemetryVersion() (int, bool) { if cc == nil { return 0, false