forked from hashicorp/terraform-ls
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Module View Command Handler (hashicorp#632)
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
Showing
7 changed files
with
285 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |