Skip to content

Commit

Permalink
Module View Command Handler (hashicorp#632)
Browse files Browse the repository at this point in the history
Adds a handler to return a list of modules used by a given module path. Each module returned shows the module name, version, documentation link and what type of module it is (local, github, or terraform registry). It also detects if a module has nested modules, and adds them in the `DependentModules` property.

This is meant to be used in tandem with hashicorp/vscode-terraform#746

TODO: It is technically incorrect to use the package hashicorp/terraform-registry-address here as it is written to parse Terraform provider addresses and may not work correctly on Terraform module addresses. The proper approach is to create a new parsing library that is dedicated to parsing these kinds of addresses correctly, by re-using the logic defined in the authorative source: hashicorp/terraform/internal/addrs/module_source.go. However this works enough for now to identify module types for display in vscode-terraform.

This also increases the CI timeout for the build process. We think the introduction of go-getter inflated the dependency tree and adds more time. At the moment it is more economical to eat the build time than take the engineering time to copy the methods needed to detect the correct module sources.
  • Loading branch information
jpogran authored Sep 23, 2021
1 parent 0d11d8b commit d202fd5
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ env:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 25
steps:
-
name: Checkout
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ require (
github.com/fsnotify/fsnotify v1.5.1
github.com/google/go-cmp v0.5.6
github.com/google/uuid v1.2.0 // indirect
github.com/hashicorp/go-getter v1.5.8
github.com/hashicorp/go-memdb v1.3.2
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.3.0
github.com/hashicorp/hcl-lang v0.0.0-20210823185445-8fcbc27a6a22
github.com/hashicorp/hcl/v2 v2.10.1
github.com/hashicorp/terraform-exec v0.14.0
github.com/hashicorp/terraform-json v0.12.0
github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896
github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045
github.com/hashicorp/terraform-schema v0.0.0-20210823185306-e7a9c4e84cd1
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,9 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-getter v1.5.3 h1:NF5+zOlQegim+w/EUhSLh6QhXHmZMEeHLQzllkQ3ROU=
github.com/hashicorp/go-getter v1.5.3/go.mod h1:BrrV/1clo8cCYu6mxvboYg+KutTiFnXjMEgDD8+i7ZI=
github.com/hashicorp/go-getter v1.5.8 h1:qx5CZXxXz5YFpALPkbf/F1iZZoRE+f6T1i/AWw/Zkic=
github.com/hashicorp/go-getter v1.5.8/go.mod h1:BrrV/1clo8cCYu6mxvboYg+KutTiFnXjMEgDD8+i7ZI=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.0 h1:8exGP7ego3OmkfksihtSouGMZ+hQrhxx+FVELeXpVPE=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
Expand Down Expand Up @@ -202,8 +203,9 @@ github.com/hashicorp/terraform-exec v0.14.0 h1:UQoUcxKTZZXhyyK68Cwn4mApT4mnFPmEX
github.com/hashicorp/terraform-exec v0.14.0/go.mod h1:qrAASDq28KZiMPDnQ02sFS9udcqEkRly002EA2izXTA=
github.com/hashicorp/terraform-json v0.12.0 h1:8czPgEEWWPROStjkWPUnTQDXmpmZPlkQAwYYLETaTvw=
github.com/hashicorp/terraform-json v0.12.0/go.mod h1:pmbq9o4EuL43db5+0ogX10Yofv1nozM+wskr/bGFJpI=
github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896 h1:1FGtlkJw87UsTMg5s8jrekrHmUPUJaMcu6ELiVhQrNw=
github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896/go.mod h1:bzBPnUIkI0RxauU8Dqo+2KrZZ28Cf48s8V6IHt3p4co=
github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045 h1:R/I8ofvXuPcTNoc//N4ruvaHGZcShI/VuU2iXo875Lo=
github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045/go.mod h1:anRyJbe12BZscpFgaeGu9gH12qfdBP094LYFtuAFzd4=
github.com/hashicorp/terraform-schema v0.0.0-20210823185306-e7a9c4e84cd1 h1:K6wkKTi4+aSYXDFGbGWgd3sP+gWTTM9VYOVeCXjJJm8=
github.com/hashicorp/terraform-schema v0.0.0-20210823185306-e7a9c4e84cd1/go.mod h1:wG+IttAk2LqgHE76fD0wt2kucaKzV7pzm7OTqCJJC3M=
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0=
Expand Down
139 changes: 139 additions & 0 deletions internal/langserver/handlers/command/module_calls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package command

import (
"context"
"fmt"
"sort"
"strings"

"github.com/creachadair/jrpc2/code"
lsctx "github.com/hashicorp/terraform-ls/internal/context"
"github.com/hashicorp/terraform-ls/internal/langserver/cmd"
"github.com/hashicorp/terraform-ls/internal/terraform/datadir"
"github.com/hashicorp/terraform-ls/internal/uri"
)

const moduleCallsVersion = 0

type moduleCallsResponse struct {
FormatVersion int `json:"v"`
ModuleCalls []moduleCall `json:"module_calls"`
}

type moduleCall struct {
Name string `json:"name"`
SourceAddr string `json:"source_addr"`
Version string `json:"version,omitempty"`
SourceType datadir.ModuleType `json:"source_type,omitempty"`
DocsLink string `json:"docs_link,omitempty"`
DependentModules []moduleCall `json:"dependent_modules"`
}

func ModuleCallsHandler(ctx context.Context, args cmd.CommandArgs) (interface{}, error) {
response := moduleCallsResponse{
FormatVersion: moduleCallsVersion,
ModuleCalls: make([]moduleCall, 0),
}

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
}

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

if found.ModManifest == nil {
return response, nil
}

response.ModuleCalls = parseModuleRecords(found.ModManifest.Records)

return response, nil
}

func parseModuleRecords(records []datadir.ModuleRecord) []moduleCall {
// sort all records by key so that dependent modules are found
// after primary modules
sort.SliceStable(records, func(i, j int) bool {
return records[i].Key < records[j].Key
})

modules := make(map[string]moduleCall)
for _, manifest := range records {
if manifest.IsRoot() {
// this is the current directory, which is technically a module
// skipping as it's not relevant in the activity bar (yet?)
continue
}

moduleName := manifest.Key
subModuleName := ""

// determine if this module is nested in another module
// in the currecnt workspace by finding a period in the moduleName
// is it better to look at SourceAddr and compare?
if strings.Contains(manifest.Key, ".") {
v := strings.Split(manifest.Key, ".")
moduleName = v[0]
subModuleName = v[1]
}

// build what we know
moduleInfo := moduleCall{
Name: moduleName,
SourceAddr: manifest.SourceAddr,
DocsLink: getModuleDocumentationLink(manifest),
Version: manifest.VersionStr,
SourceType: manifest.GetModuleType(),
DependentModules: make([]moduleCall, 0),
}

m, present := modules[moduleName]
if present {
// this module is located inside another so append
moduleInfo.Name = subModuleName
m.DependentModules = append(m.DependentModules, moduleInfo)
modules[moduleName] = m
} else {
// this is the first we've seen module
modules[moduleName] = moduleInfo
}
}

// don't need the map anymore, return a list of modules found
list := make([]moduleCall, 0)
for _, mo := range modules {
list = append(list, mo)
}

sort.SliceStable(list, func(i, j int) bool {
return list[i].Name < list[j].Name
})

return list
}

func getModuleDocumentationLink(record datadir.ModuleRecord) string {
if record.GetModuleType() != datadir.TFREGISTRY {
return ""
}

return fmt.Sprintf(`https://registry.terraform.io/modules/%s/%s`, record.SourceAddr, record.VersionStr)
}
89 changes: 89 additions & 0 deletions internal/langserver/handlers/command/module_calls_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package command

import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-ls/internal/terraform/datadir"
)

func Test_parseModuleRecords(t *testing.T) {
tests := []struct {
name string
records []datadir.ModuleRecord
want []moduleCall
}{
{
name: "detects terraform module types",
records: []datadir.ModuleRecord{
{
Key: "ec2_instances",
SourceAddr: "terraform-aws-modules/ec2-instance/aws",
VersionStr: "2.12.0",
Dir: ".terraform\\modules\\ec2_instances",
},
{
Key: "web_server_sg",
SourceAddr: "github.com/terraform-aws-modules/terraform-aws-security-group",
VersionStr: "",
Dir: ".terraform\\modules\\web_server_sg",
},
{
Key: "eks",
SourceAddr: "terraform-aws-modules/eks/aws",
VersionStr: "17.20.0",
Dir: ".terraform\\modules\\eks",
},
{
Key: "eks.fargate",
SourceAddr: "./modules/fargate",
VersionStr: "",
Dir: ".terraform\\modules\\eks\\modules\\fargate",
},
},
want: []moduleCall{
{
Name: "ec2_instances",
SourceAddr: "terraform-aws-modules/ec2-instance/aws",
Version: "2.12.0",
SourceType: "tfregistry",
DocsLink: "https://registry.terraform.io/modules/terraform-aws-modules/ec2-instance/aws/2.12.0",
DependentModules: []moduleCall{},
},
{
Name: "eks",
SourceAddr: "terraform-aws-modules/eks/aws",
Version: "17.20.0",
SourceType: "tfregistry",
DocsLink: "https://registry.terraform.io/modules/terraform-aws-modules/eks/aws/17.20.0",
DependentModules: []moduleCall{
{
Name: "fargate",
SourceAddr: "./modules/fargate",
Version: "",
SourceType: "local",
DocsLink: "",
DependentModules: []moduleCall{},
},
},
},
{
Name: "web_server_sg",
SourceAddr: "github.com/terraform-aws-modules/terraform-aws-security-group",
Version: "",
SourceType: "github",
DocsLink: "",
DependentModules: []moduleCall{},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseModuleRecords(tt.records)
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Fatalf("module mismatch: %s", diff)
}
})
}
}
1 change: 1 addition & 0 deletions internal/langserver/handlers/execute_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var handlers = cmd.Handlers{
cmd.Name("module.callers"): command.ModuleCallersHandler,
cmd.Name("terraform.init"): command.TerraformInitHandler,
cmd.Name("terraform.validate"): command.TerraformValidateHandler,
cmd.Name("module.calls"): command.ModuleCallsHandler,
}

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

import (
"github.com/hashicorp/go-getter"
tfregistry "github.com/hashicorp/terraform-registry-address"
)

type ModuleType string

const (
UNKNOWN ModuleType = "unknown"
TFREGISTRY ModuleType = "tfregistry"
LOCAL ModuleType = "local"
GITHUB ModuleType = "github"
GIT ModuleType = "git"
)

// GetModuleType parses source addresses to determine what kind of source the Terraform module comes
// from. It currently supports detecting Terraform Registry modules, GitHub modules, Git modules, and
// local file paths
func (r *ModuleRecord) GetModuleType() ModuleType {
// TODO: It is technically incorrect to use the package hashicorp/terraform-registry-address
// here as it is written to parse Terraform provider addresses and may not work correctly on
// Terraform module addresses. The proper approach is to create a new parsing library that is
// dedicated to parsing these kinds of addresses correctly, by re-using the logic defined in
// the authorative source: hashicorp/terraform/internal/addrs/module_source.go.
// However this works enough for now to identify module types for display in vscode-terraform.
// Example: terraform-aws-modules/ec2-instance/aws
if _, err := tfregistry.ParseRawProviderSourceString(r.SourceAddr); err == nil {
return TFREGISTRY
}

// Example: github.com/terraform-aws-modules/terraform-aws-security-group
if _, ok, _ := new(getter.GitHubDetector).Detect(r.SourceAddr, ""); ok {
return GITHUB
}

// Example: git::https://example.com/vpc.git
if _, ok, _ := new(getter.GitDetector).Detect(r.SourceAddr, ""); ok {
return GIT
}

// Local, non relative, file paths
if _, ok, _ := new(getter.FileDetector).Detect(r.SourceAddr, ""); ok {
return LOCAL
}

return UNKNOWN
}

0 comments on commit d202fd5

Please sign in to comment.