Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New command: inspect-module #231

Merged
merged 1 commit into from
Jul 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions commands/inspect_module_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package commands

import (
"context"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"syscall"

"github.com/hashicorp/go-multierror"
ictx "github.com/hashicorp/terraform-ls/internal/context"
"github.com/hashicorp/terraform-ls/internal/terraform/rootmodule"
"github.com/hashicorp/terraform-ls/logging"
"github.com/mitchellh/cli"
)

type InspectModuleCommand struct {
Ui cli.Ui
Verbose bool

logger *log.Logger
}

func (c *InspectModuleCommand) flags() *flag.FlagSet {
fs := defaultFlagSet("debug")
fs.BoolVar(&c.Verbose, "verbose", false, "whether to enable verbose output")
fs.Usage = func() { c.Ui.Error(c.Help()) }
return fs
}

func (c *InspectModuleCommand) Run(args []string) int {
f := c.flags()
if err := f.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s", err))
return 1
}

if f.NArg() != 1 {
c.Ui.Output(fmt.Sprintf("expected exactly 1 argument (%d given): %q",
f.NArg(), c.flags().Args()))
return 1
}

path := f.Arg(0)

var logDestination io.Writer
if c.Verbose {
logDestination = os.Stderr
} else {
logDestination = ioutil.Discard
}

c.logger = logging.NewLogger(logDestination)

err := c.inspect(path)
if err != nil {
c.Ui.Output(err.Error())
return 1
}

return 0
}

func (c *InspectModuleCommand) inspect(rootPath string) error {
rootPath, err := filepath.Abs(rootPath)
if err != nil {
return err
}

fi, err := os.Stat(rootPath)
if err != nil {
return err
}

if !fi.IsDir() {
return fmt.Errorf("expected %s to be a directory", rootPath)
}

rmm := rootmodule.NewRootModuleManager()
rmm.SetLogger(c.logger)
walker := rootmodule.NewWalker()
walker.SetLogger(c.logger)

ctx, cancel := ictx.WithSignalCancel(context.Background(),
c.logger, syscall.SIGINT, syscall.SIGTERM)
defer cancel()

err = walker.StartWalking(ctx, rootPath, func(ctx context.Context, dir string) error {
rm, err := rmm.AddAndStartLoadingRootModule(ctx, dir)
if err != nil {
return err
}
<-rm.LoadingDone()

return nil
})
if err != nil {
return err
}

<-walker.Done()

modules := rmm.ListRootModules()
c.Ui.Output(fmt.Sprintf("%d root modules found in total at %s", len(modules), rootPath))
for _, rm := range modules {
errs := &multierror.Error{}

err := rm.LoadError()
if err != nil {
var ok bool
errs, ok = err.(*multierror.Error)
if !ok {
return err
}
}
errs.ErrorFormat = formatErrors

modules := formatModuleRecords(rm.Modules())
subModules := fmt.Sprintf("%d modules", len(modules))
if len(modules) > 0 {
subModules += "\n"
for _, m := range modules {
subModules += fmt.Sprintf(" - %s", m)
}
}

c.Ui.Output(fmt.Sprintf(` - %s
- %s
- %s`, rm.Path(), errs, subModules))
}
c.Ui.Output("")

return nil
}

func formatErrors(errors []error) string {
if len(errors) == 0 {
return "0 errors"
}

out := fmt.Sprintf("%d errors:\n", len(errors))
for _, err := range errors {
out += fmt.Sprintf(" - %s\n", err)
}
return strings.TrimSpace(out)
}

func formatModuleRecords(mds []rootmodule.ModuleRecord) []string {
out := make([]string, 0)
for _, m := range mds {
if m.IsRoot() {
continue
}
if m.IsExternal() {
out = append(out, "EXTERNAL(%s)", m.SourceAddr)
continue
}
out = append(out, fmt.Sprintf("%s (%s)", m.Dir, m.SourceAddr))
}
return out
}

func (c *InspectModuleCommand) Help() string {
helpText := `
Usage: terraform-ls inspect-module [path]

` + c.Synopsis() + "\n\n" + helpForFlags(c.flags())
return strings.TrimSpace(helpText)
}

func (c *InspectModuleCommand) Synopsis() string {
return "Lists available debug items"
}
29 changes: 29 additions & 0 deletions docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,32 @@ $ terraform-ls serve \
```

The target file will be truncated before being written into.

## "No root module found for ... functionality may be limited"

Most of the language server features depend on initialized root modules
(i.e. folder with `*.tf` files where you ran `terraform init` successfully).
and server's ability to discover them within the hierarchy and match them
with files being open in the editor.

This functionality should cover many hierarchies, but it may not cover yours.
If it appears that root modules aren't being discovered or matched the way
they should be, it can be useful to use `inspect-module` to obtain
the discovery results and provide them to maintainers in a bug report.

Point it to the same directory that you tried to open in your IDE/editor
and wait for the output - it may take some seconds or low minutes
depending on the complexity of your hierarchy and number of root modules in it.

```
$ terraform-ls inspect-module /path/to/dir
```

## "Unable to retrieve schemas for ..."

The process of obtaining the schema currently requires access to the state,
which in turn means that if the code itself doesn't have enough context
to obtain the state and/or there isn't context available from config file(s)
in standard locations you may need to provide that extra context.

See https://github.com/hashicorp/terraform-ls/issues/128 for more.
26 changes: 24 additions & 2 deletions internal/terraform/rootmodule/root_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type rootModule struct {
// loading
isLoading bool
isLoadingMu *sync.RWMutex
loadingDone <-chan struct{}
cancelLoading context.CancelFunc
loadErr error
loadErrMu *sync.RWMutex
Expand Down Expand Up @@ -146,17 +147,32 @@ func (rm *rootModule) discoverModuleCache(dir string) error {
return nil
}

func (rm *rootModule) Modules() []ModuleRecord {
rm.moduleMu.Lock()
defer rm.moduleMu.Unlock()
if rm.moduleManifest == nil {
return []ModuleRecord{}
}

return rm.moduleManifest.Records
}

func (rm *rootModule) SetLogger(logger *log.Logger) {
rm.logger = logger
}

func (rm *rootModule) StartLoading() {
func (rm *rootModule) StartLoading() error {
if !rm.IsLoadingDone() {
return fmt.Errorf("root module is already being loaded")
}
ctx, cancelFunc := context.WithCancel(context.Background())
rm.cancelLoading = cancelFunc
rm.loadingDone = ctx.Done()

go func(ctx context.Context) {
rm.setLoadErr(rm.load(ctx))
}(ctx)
return nil
}

func (rm *rootModule) CancelLoading() {
Expand All @@ -166,6 +182,10 @@ func (rm *rootModule) CancelLoading() {
rm.setLoadingState(false)
}

func (rm *rootModule) LoadingDone() <-chan struct{} {
return rm.loadingDone
}

func (rm *rootModule) load(ctx context.Context) error {
var errs *multierror.Error
defer rm.CancelLoading()
Expand Down Expand Up @@ -327,7 +347,7 @@ func (rm *rootModule) UpdateModuleManifest(lockFile File) error {

mm, err := ParseModuleManifestFromFile(lockFile.Path())
if err != nil {
return err
return fmt.Errorf("failed to update module manifest: %w", err)
}

rm.moduleManifest = mm
Expand Down Expand Up @@ -376,6 +396,8 @@ func (rm *rootModule) setSchemaLoaded(isLoaded bool) {
}

func (rm *rootModule) ReferencesModulePath(path string) bool {
rm.moduleMu.Lock()
defer rm.moduleMu.Unlock()
if rm.moduleManifest == nil {
return false
}
Expand Down
12 changes: 11 additions & 1 deletion internal/terraform/rootmodule/root_module_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ func (rmm *rootModuleManager) AddAndStartLoadingRootModule(ctx context.Context,
}

rmm.logger.Printf("asynchronously loading root module %s", dir)
rm.StartLoading()
err = rm.StartLoading()
if err != nil {
return rm, err
}

return rm, nil
}
Expand Down Expand Up @@ -145,6 +148,13 @@ func (rmm *rootModuleManager) RootModuleCandidatesByPath(path string) RootModule
return candidates
}

func (rmm *rootModuleManager) ListRootModules() RootModules {
modules := make([]RootModule, 0)
for _, rm := range rmm.rms {
modules = append(modules, rm)
}
return modules
}
func (rmm *rootModuleManager) RootModuleByPath(path string) (RootModule, error) {
candidates := rmm.RootModuleCandidatesByPath(path)
if len(candidates) > 0 {
Expand Down
4 changes: 3 additions & 1 deletion internal/terraform/rootmodule/root_module_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,9 @@ func TestRootModuleManager_RootModuleCandidatesByPath(t *testing.T) {
t.Run(fmt.Sprintf("%d-%s/%s", i, tc.name, base), func(t *testing.T) {
rmm := testRootModuleManager(t)
w := MockWalker()
err := w.StartWalking(tc.walkerRoot, func(ctx context.Context, rmPath string) error {
w.SetLogger(testLogger())
ctx := context.Background()
err := w.StartWalking(ctx, tc.walkerRoot, func(ctx context.Context, rmPath string) error {
_, err := rmm.AddAndStartLoadingRootModule(ctx, rmPath)
return err
})
Expand Down
5 changes: 4 additions & 1 deletion internal/terraform/rootmodule/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type RootModuleManager interface {
SetTerraformExecTimeout(timeout time.Duration)

AddAndStartLoadingRootModule(ctx context.Context, dir string) (RootModule, error)
ListRootModules() RootModules
PathsToWatch() []string
RootModuleByPath(path string) (RootModule, error)
CancelLoading()
Expand All @@ -60,8 +61,9 @@ func (rms RootModules) Paths() []string {
type RootModule interface {
Path() string
LoadError() error
StartLoading()
StartLoading() error
IsLoadingDone() bool
LoadingDone() <-chan struct{}
IsKnownPluginLockFile(path string) bool
IsKnownModuleManifestFile(path string) bool
PathsToWatch() []string
Expand All @@ -72,6 +74,7 @@ type RootModule interface {
IsParserLoaded() bool
TerraformFormatter() (exec.Formatter, error)
IsTerraformLoaded() bool
Modules() []ModuleRecord
}

type RootModuleFactory func(context.Context, string) (*rootModule, error)
Expand Down
Loading