Skip to content

Commit

Permalink
Introduce list module providers command (#712)
Browse files Browse the repository at this point in the history
* add new module providers workspace command

* add state store to sevice and session mocking

* add integration test for module providers command

* Review feedback

* add docs link

* check meta data state

* Apply suggestions from code review

Co-authored-by: Radek Simko <radek.simko@gmail.com>

Co-authored-by: Radek Simko <radek.simko@gmail.com>
  • Loading branch information
dbanck and radeksimko authored Nov 16, 2021
1 parent 57f6f05 commit 1620784
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 10 deletions.
85 changes: 85 additions & 0 deletions internal/langserver/handlers/command/module_providers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package command

import (
"context"
"fmt"

"github.com/creachadair/jrpc2/code"
lsctx "github.com/hashicorp/terraform-ls/internal/context"
"github.com/hashicorp/terraform-ls/internal/langserver/cmd"
op "github.com/hashicorp/terraform-ls/internal/terraform/module/operation"
"github.com/hashicorp/terraform-ls/internal/uri"
tfaddr "github.com/hashicorp/terraform-registry-address"
)

const moduleProvidersVersion = 0

type moduleProvidersResponse struct {
FormatVersion int `json:"v"`
ProviderRequirements map[string]providerRequirement `json:"provider_requirements"`
InstalledProviders map[string]string `json:"installed_providers"`
}

type providerRequirement struct {
DisplayName string `json:"display_name"`
VersionConstraint string `json:"version_constraint,omitempty"`
DocsLink string `json:"docs_link,omitempty"`
}

func ModuleProvidersHandler(ctx context.Context, args cmd.CommandArgs) (interface{}, error) {
response := moduleProvidersResponse{
FormatVersion: moduleProvidersVersion,
ProviderRequirements: make(map[string]providerRequirement),
InstalledProviders: make(map[string]string),
}

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
}

mm, err := lsctx.ModuleFinder(ctx)
if err != nil {
return response, err
}

mod, _ := mm.ModuleByPath(modPath)
if mod == nil {
return response, nil
}

if mod.MetaState == op.OpStateUnknown || mod.MetaErr != nil {
return response, nil
}

for provider, version := range mod.Meta.ProviderRequirements {
response.ProviderRequirements[provider.String()] = providerRequirement{
DisplayName: provider.ForDisplay(),
VersionConstraint: version.String(),
DocsLink: getProviderDocumentationLink(provider),
}
}

for provider, version := range mod.InstalledProviders {
response.InstalledProviders[provider.String()] = version.String()
}

return response, nil
}

func getProviderDocumentationLink(provider tfaddr.Provider) string {
if provider.IsLegacy() || provider.IsBuiltIn() || provider.Hostname != "registry.terraform.io" {
return ""
}

return fmt.Sprintf(`https://registry.terraform.io/providers/%s/latest`, provider.ForDisplay())
}
1 change: 1 addition & 0 deletions internal/langserver/handlers/execute_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var handlers = cmd.Handlers{
cmd.Name("terraform.init"): command.TerraformInitHandler,
cmd.Name("terraform.validate"): command.TerraformValidateHandler,
cmd.Name("module.calls"): command.ModuleCallsHandler,
cmd.Name("module.providers"): command.ModuleProvidersHandler,
}

func (lh *logHandler) WorkspaceExecuteCommand(ctx context.Context, params lsp.ExecuteCommandParams) (interface{}, error) {
Expand Down
162 changes: 162 additions & 0 deletions internal/langserver/handlers/execute_command_module_providers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package handlers

import (
"fmt"
"testing"

"github.com/creachadair/jrpc2/code"
"github.com/hashicorp/go-version"
"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"
tfaddr "github.com/hashicorp/terraform-registry-address"
tfmod "github.com/hashicorp/terraform-schema/module"
"github.com/stretchr/testify/mock"
)

func TestLangServer_workspaceExecuteCommand_moduleProviders_argumentError(t *testing.T) {
rootDir := t.TempDir()
rootUri := uri.FromPath(rootDir)

ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{
TerraformCalls: &exec.TerraformMockCalls{
PerWorkDir: map[string][]*mock.Call{
rootDir: validTfMockCalls(),
},
},
}))
stop := ls.Start(t)
defer stop()

ls.Call(t, &langserver.CallRequest{
Method: "initialize",
ReqParams: fmt.Sprintf(`{
"capabilities": {},
"rootUri": %q,
"processId": 12345
}`, rootUri)})
ls.Notify(t, &langserver.CallRequest{
Method: "initialized",
ReqParams: "{}",
})
ls.Call(t, &langserver.CallRequest{
Method: "textDocument/didOpen",
ReqParams: fmt.Sprintf(`{
"textDocument": {
"version": 0,
"languageId": "terraform",
"text": "provider \"github\" {}",
"uri": %q
}
}`, fmt.Sprintf("%s/main.tf", rootUri))})

ls.CallAndExpectError(t, &langserver.CallRequest{
Method: "workspace/executeCommand",
ReqParams: fmt.Sprintf(`{
"command": %q
}`, cmd.Name("module.providers"))}, code.InvalidParams.Err())
}

func TestLangServer_workspaceExecuteCommand_moduleProviders_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"),
ProviderRequirements: map[tfaddr.Provider]version.Constraints{
tfaddr.NewDefaultProvider("aws"): testConstraint(t, "1.2.3"),
tfaddr.NewDefaultProvider("google"): testConstraint(t, ">= 2.0.0"),
},
ProviderReferences: map[tfmod.ProviderRef]tfaddr.Provider{
{LocalName: "aws"}: tfaddr.NewDefaultProvider("aws"),
{LocalName: "google"}: tfaddr.NewDefaultProvider("google"),
},
}

err = s.Modules.UpdateMetadata(modDir, metadata, nil)
if err != nil {
t.Fatal(err)
}

pVersions := map[tfaddr.Provider]*version.Version{
tfaddr.NewDefaultProvider("aws"): version.Must(version.NewVersion("1.2.3")),
tfaddr.NewDefaultProvider("google"): version.Must(version.NewVersion("2.5.5")),
}
err = s.Modules.UpdateInstalledProviders(modDir, pVersions)
if err != nil {
t.Fatal(err)
}

ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{
TerraformCalls: &exec.TerraformMockCalls{
PerWorkDir: map[string][]*mock.Call{
modDir: validTfMockCalls(),
},
},
StateStore: s,
}))
stop := ls.Start(t)
defer stop()

ls.Call(t, &langserver.CallRequest{
Method: "initialize",
ReqParams: fmt.Sprintf(`{
"capabilities": {},
"rootUri": %q,
"processId": 12345
}`, 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.providers"), modUri)}, `{
"jsonrpc": "2.0",
"id": 2,
"result": {
"v": 0,
"provider_requirements": {
"registry.terraform.io/hashicorp/aws": {
"display_name": "hashicorp/aws",
"version_constraint":"1.2.3",
"docs_link": "https://registry.terraform.io/providers/hashicorp/aws/latest"
},
"registry.terraform.io/hashicorp/google": {
"display_name": "hashicorp/google",
"version_constraint": "\u003e= 2.0.0",
"docs_link": "https://registry.terraform.io/providers/hashicorp/google/latest"
}
},
"installed_providers":{
"registry.terraform.io/hashicorp/aws": "1.2.3",
"registry.terraform.io/hashicorp/google": "2.5.5"
}
}
}`)
}

func testConstraint(t *testing.T, v string) version.Constraints {
constraints, err := version.NewConstraint(v)
if err != nil {
t.Fatal(err)
}
return constraints
}
25 changes: 15 additions & 10 deletions internal/langserver/handlers/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type service struct {
tfExecOpts *exec.ExecutorOpts
telemetry telemetry.Sender
decoder *decoder.Decoder
stateStore *state.StateStore

additionalHandlers map[string]rpch.Func
}
Expand Down Expand Up @@ -434,29 +435,33 @@ func (svc *service) configureSessionDependencies(ctx context.Context, cfgOpts *s
svc.sessCtx = exec.WithExecutorOpts(svc.sessCtx, execOpts)
svc.sessCtx = exec.WithExecutorFactory(svc.sessCtx, svc.tfExecFactory)

store, err := state.NewStateStore()
if err != nil {
return err
if svc.stateStore == nil {
store, err := state.NewStateStore()
if err != nil {
return err
}
svc.stateStore = store
}
store.SetLogger(svc.logger)
store.Modules.ChangeHooks = state.ModuleChangeHooks{
sendModuleTelemetry(svc.sessCtx, store, svc.telemetry),

svc.stateStore.SetLogger(svc.logger)
svc.stateStore.Modules.ChangeHooks = state.ModuleChangeHooks{
sendModuleTelemetry(svc.sessCtx, svc.stateStore, svc.telemetry),
}

svc.modStore = store.Modules
svc.schemaStore = store.ProviderSchemas
svc.modStore = svc.stateStore.Modules
svc.schemaStore = svc.stateStore.ProviderSchemas

svc.decoder = idecoder.NewDecoder(ctx, &idecoder.PathReader{
ModuleReader: svc.modStore,
SchemaReader: svc.schemaStore,
})

err = schemas.PreloadSchemasToStore(store.ProviderSchemas)
err := schemas.PreloadSchemasToStore(svc.stateStore.ProviderSchemas)
if err != nil {
return err
}

svc.modMgr = svc.newModuleManager(svc.sessCtx, svc.fs, store.Modules, store.ProviderSchemas)
svc.modMgr = svc.newModuleManager(svc.sessCtx, svc.fs, svc.stateStore.Modules, svc.stateStore.ProviderSchemas)
svc.modMgr.SetLogger(svc.logger)

svc.walker = svc.newWalker(svc.fs, svc.modMgr)
Expand Down
5 changes: 5 additions & 0 deletions internal/langserver/handlers/session_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/creachadair/jrpc2/handler"
"github.com/hashicorp/terraform-ls/internal/filesystem"
"github.com/hashicorp/terraform-ls/internal/langserver/session"
"github.com/hashicorp/terraform-ls/internal/state"
"github.com/hashicorp/terraform-ls/internal/terraform/discovery"
"github.com/hashicorp/terraform-ls/internal/terraform/exec"
"github.com/hashicorp/terraform-ls/internal/terraform/module"
Expand All @@ -20,6 +21,7 @@ type MockSessionInput struct {
Filesystem filesystem.Filesystem
TerraformCalls *exec.TerraformMockCalls
AdditionalHandlers map[string]handler.Func
StateStore *state.StateStore
}

type mockSession struct {
Expand All @@ -44,10 +46,12 @@ func (ms *mockSession) new(srvCtx context.Context) session.Session {
var fs filesystem.Filesystem
fs = filesystem.NewFilesystem()
var handlers map[string]handler.Func
var stateStore *state.StateStore
if ms.mockInput != nil {
if ms.mockInput.Filesystem != nil {
fs = ms.mockInput.Filesystem
}
stateStore = ms.mockInput.StateStore
handlers = ms.mockInput.AdditionalHandlers
}

Expand All @@ -72,6 +76,7 @@ func (ms *mockSession) new(srvCtx context.Context) session.Session {
tfDiscoFunc: d.LookPath,
tfExecFactory: exec.NewMockExecutor(tfCalls),
additionalHandlers: handlers,
stateStore: stateStore,
}

return svc
Expand Down

0 comments on commit 1620784

Please sign in to comment.