Skip to content

Commit

Permalink
New command: inspect-module (#231)
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko authored Jul 24, 2020
1 parent bbf80a3 commit 7da9006
Show file tree
Hide file tree
Showing 10 changed files with 299 additions and 22 deletions.
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

0 comments on commit 7da9006

Please sign in to comment.