diff --git a/commands/completion_command.go b/commands/completion_command.go index 3955cd2c3..9c1f239c3 100644 --- a/commands/completion_command.go +++ b/commands/completion_command.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" "strconv" "strings" @@ -47,6 +48,13 @@ func (c *CompletionCommand) Run(args []string) int { } path := f.Arg(0) + + path, err := filepath.Abs(path) + if err != nil { + c.Ui.Output(err.Error()) + return 1 + } + content, err := ioutil.ReadFile(path) if err != nil { c.Ui.Error(fmt.Sprintf("reading file at %q failed: %s", path, err)) @@ -102,16 +110,20 @@ func (c *CompletionCommand) Run(args []string) int { w, err := rootmodule.NewRootModule(context.Background(), fh.Dir()) if err != nil { - c.Ui.Error(err.Error()) + c.Ui.Error(fmt.Sprintf("failed to load root module: %s", err.Error())) + return 1 + } + p, err := w.Parser() + if err != nil { + c.Ui.Error(fmt.Sprintf("failed to find parser: %s", err.Error())) return 1 } - p := w.Parser() pos := fPos.Position() candidates, err := p.CompletionCandidatesAtPos(hclFile, pos) if err != nil { - c.Ui.Error(err.Error()) + c.Ui.Error(fmt.Sprintf("failed to find candidates: %s", err.Error())) return 1 } diff --git a/go.mod b/go.mod index b85e84020..ee60f0354 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/creachadair/jrpc2 v0.8.1 github.com/fsnotify/fsnotify v1.4.9 github.com/google/go-cmp v0.4.0 - github.com/hashicorp/go-multierror v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.0 github.com/hashicorp/go-version v1.2.0 github.com/hashicorp/hcl/v2 v2.5.2-0.20200528183353-fa7c453538de github.com/hashicorp/terraform-json v0.5.0 diff --git a/internal/terraform/exec/exec.go b/internal/terraform/exec/exec.go index 82a87a2c8..077bc93c5 100644 --- a/internal/terraform/exec/exec.go +++ b/internal/terraform/exec/exec.go @@ -33,10 +33,9 @@ type cmdCtxFunc func(context.Context, string, ...string) *exec.Cmd // ExecutorFactory can be used in external consumers of exec pkg // to enable easy swapping with MockExecutor -type ExecutorFactory func(ctx context.Context, path string) *Executor +type ExecutorFactory func(path string) *Executor type Executor struct { - ctx context.Context timeout time.Duration execPath string @@ -55,9 +54,8 @@ type command struct { StderrBuffer *bytes.Buffer } -func NewExecutor(ctx context.Context, path string) *Executor { +func NewExecutor(path string) *Executor { return &Executor{ - ctx: ctx, timeout: defaultExecTimeout, execPath: path, logger: log.New(ioutil.Discard, "", 0), @@ -87,15 +85,14 @@ func (e *Executor) GetExecPath() string { return e.execPath } -func (e *Executor) cmd(args ...string) (*command, error) { +func (e *Executor) cmd(ctx context.Context, args ...string) (*command, error) { if e.workDir == "" { return nil, fmt.Errorf("no work directory set") } - ctx := e.ctx cancel := func() {} if e.timeout > 0 { - ctx, cancel = context.WithTimeout(e.ctx, e.timeout) + ctx, cancel = context.WithTimeout(ctx, e.timeout) } var outBuf bytes.Buffer @@ -186,8 +183,8 @@ func (e *Executor) runCmd(command *command) ([]byte, error) { return e.waitCmd(command) } -func (e *Executor) run(args ...string) ([]byte, error) { - cmd, err := e.cmd(args...) +func (e *Executor) run(ctx context.Context, args ...string) ([]byte, error) { + cmd, err := e.cmd(ctx, args...) e.logger.Printf("running with timeout %s", e.timeout) defer cmd.CancelFunc() if err != nil { @@ -196,8 +193,8 @@ func (e *Executor) run(args ...string) ([]byte, error) { return e.runCmd(cmd) } -func (e *Executor) Format(input []byte) ([]byte, error) { - cmd, err := e.cmd("fmt", "-") +func (e *Executor) Format(ctx context.Context, input []byte) ([]byte, error) { + cmd, err := e.cmd(ctx, "fmt", "-") if err != nil { return nil, err } @@ -236,8 +233,8 @@ func writeAndClose(w io.WriteCloser, input []byte) (int, error) { return n, nil } -func (e *Executor) Version() (string, error) { - out, err := e.run("version") +func (e *Executor) Version(ctx context.Context) (string, error) { + out, err := e.run(ctx, "version") if err != nil { return "", fmt.Errorf("failed to get version: %w", err) } @@ -251,8 +248,8 @@ func (e *Executor) Version() (string, error) { return version, nil } -func (e *Executor) VersionIsSupported(c version.Constraints) error { - v, err := e.Version() +func (e *Executor) VersionIsSupported(ctx context.Context, c version.Constraints) error { + v, err := e.Version(ctx) if err != nil { return err } @@ -269,8 +266,8 @@ func (e *Executor) VersionIsSupported(c version.Constraints) error { return nil } -func (e *Executor) ProviderSchemas() (*tfjson.ProviderSchemas, error) { - outBytes, err := e.run("providers", "schema", "-json") +func (e *Executor) ProviderSchemas(ctx context.Context) (*tfjson.ProviderSchemas, error) { + outBytes, err := e.run(ctx, "providers", "schema", "-json") if err != nil { return nil, fmt.Errorf("failed to get schemas: %w", err) } diff --git a/internal/terraform/exec/exec_mock.go b/internal/terraform/exec/exec_mock.go index 93025dd35..11e40462d 100644 --- a/internal/terraform/exec/exec_mock.go +++ b/internal/terraform/exec/exec_mock.go @@ -78,7 +78,7 @@ func (mc *MockQueue) NextMockItem() *MockItem { } func MockExecutor(md MockItemDispenser) ExecutorFactory { - return func(ctx context.Context, path string) *Executor { + return func(path string) *Executor { if md == nil { md = &MockCall{ MockError: "no mocks provided", @@ -86,7 +86,7 @@ func MockExecutor(md MockItemDispenser) ExecutorFactory { } path, ctxFunc := mockCommandCtxFunc(md) - executor := NewExecutor(context.Background(), path) + executor := NewExecutor(path) executor.cmdCtxFunc = ctxFunc return executor } diff --git a/internal/terraform/exec/exec_test.go b/internal/terraform/exec/exec_test.go index 5bcffd5c9..5d6a4a30a 100644 --- a/internal/terraform/exec/exec_test.go +++ b/internal/terraform/exec/exec_test.go @@ -14,13 +14,13 @@ func TestExec_timeout(t *testing.T) { Args: []string{"version"}, SleepDuration: 100 * time.Millisecond, Stdout: "Terraform v0.12.0\n", - })(context.Background(), "") + })("") e.SetWorkdir(os.TempDir()) e.timeout = 1 * time.Millisecond expectedErr := ExecTimeoutError([]string{"terraform", "version"}, e.timeout) - _, err := e.Version() + _, err := e.Version(context.Background()) if err != nil { if errors.Is(err, expectedErr) { return @@ -38,9 +38,9 @@ func TestExec_Version(t *testing.T) { Args: []string{"version"}, Stdout: "Terraform v0.12.0\n", ExitCode: 0, - })(context.Background(), "") + })("") e.SetWorkdir(os.TempDir()) - v, err := e.Version() + v, err := e.Version(context.Background()) if err != nil { t.Fatal(err) } @@ -55,9 +55,9 @@ func TestExec_Format(t *testing.T) { Args: []string{"fmt", "-"}, Stdout: string(expectedOutput), ExitCode: 0, - })(context.Background(), "") + })("") e.SetWorkdir(os.TempDir()) - out, err := e.Format([]byte("unformatted")) + out, err := e.Format(context.Background(), []byte("unformatted")) if err != nil { t.Fatal(err) } @@ -73,10 +73,10 @@ func TestExec_ProviderSchemas(t *testing.T) { Args: []string{"providers", "schema", "-json"}, Stdout: `{"format_version": "0.1"}`, ExitCode: 0, - })(context.Background(), "") + })("") e.SetWorkdir(os.TempDir()) - ps, err := e.ProviderSchemas() + ps, err := e.ProviderSchemas(context.Background()) if err != nil { t.Fatal(err) } diff --git a/internal/terraform/rootmodule/file.go b/internal/terraform/rootmodule/file.go new file mode 100644 index 000000000..ebe9c5bdb --- /dev/null +++ b/internal/terraform/rootmodule/file.go @@ -0,0 +1,43 @@ +package rootmodule + +import ( + "fmt" + "os" +) + +func findFile(paths []string) (File, error) { + var lf File + var err error + + for _, path := range paths { + lf, err = newFile(path) + if err == nil { + return lf, nil + } + if !os.IsNotExist(err) { + return nil, err + } + } + + return nil, err +} + +type file struct { + path string +} + +func (f *file) Path() string { + return f.path +} + +func newFile(path string) (File, error) { + fi, err := os.Stat(path) + if err != nil { + return nil, err + } + if fi.IsDir() { + return nil, fmt.Errorf("expected %s to be a file, not a dir", path) + } + + return &file{path: path}, nil +} diff --git a/internal/terraform/rootmodule/root_module.go b/internal/terraform/rootmodule/root_module.go index 7fbdcc6a4..a6351065a 100644 --- a/internal/terraform/rootmodule/root_module.go +++ b/internal/terraform/rootmodule/root_module.go @@ -2,6 +2,7 @@ package rootmodule import ( "context" + "errors" "fmt" "io/ioutil" "log" @@ -10,6 +11,7 @@ import ( "sync" "time" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-ls/internal/terraform/discovery" "github.com/hashicorp/terraform-ls/internal/terraform/exec" "github.com/hashicorp/terraform-ls/internal/terraform/lang" @@ -17,43 +19,68 @@ import ( ) type rootModule struct { - ctx context.Context - path string - logger *log.Logger - pluginLockFile File + path string + logger *log.Logger + + // loading + isLoading bool + isLoadingMu *sync.RWMutex + cancelLoading context.CancelFunc + loadErr error + loadErrMu *sync.RWMutex + + // module cache + moduleMu *sync.RWMutex moduleManifestFile File moduleManifest *moduleManifest - tfVersion string - tfDiscoFunc discovery.DiscoveryFunc - tfNewExecutor exec.ExecutorFactory - tfExecPath string - tfExecTimeout time.Duration - tfExecLogPath string - newSchemaStorage schema.StorageFactory - ignorePluginCache bool - - tfExec *exec.Executor - parser lang.Parser - schemaWriter schema.Writer - pluginMu *sync.RWMutex - moduleMu *sync.RWMutex + // plugin cache + pluginMu *sync.RWMutex + pluginLockFile File + newSchemaStorage schema.StorageFactory + schemaStorage *schema.Storage + schemaLoaded bool + schemaLoadedMu *sync.RWMutex + + // terraform executor + tfLoaded bool + tfLoadedMu *sync.RWMutex + tfExec *exec.Executor + tfNewExecutor exec.ExecutorFactory + tfExecPath string + tfExecTimeout time.Duration + tfExecLogPath string + + // terraform discovery + tfDiscoFunc discovery.DiscoveryFunc + tfDiscoErr error + tfVersion string + tfVersionErr error + + // language parser + parserLoaded bool + parserLoadedMu *sync.RWMutex + parser lang.Parser } -func newRootModule(ctx context.Context, dir string) *rootModule { +func newRootModule(dir string) *rootModule { return &rootModule{ - ctx: ctx, - path: dir, - logger: defaultLogger, - pluginMu: &sync.RWMutex{}, - moduleMu: &sync.RWMutex{}, + path: dir, + logger: defaultLogger, + isLoadingMu: &sync.RWMutex{}, + loadErrMu: &sync.RWMutex{}, + moduleMu: &sync.RWMutex{}, + pluginMu: &sync.RWMutex{}, + schemaLoadedMu: &sync.RWMutex{}, + tfLoadedMu: &sync.RWMutex{}, + parserLoadedMu: &sync.RWMutex{}, } } var defaultLogger = log.New(ioutil.Discard, "", 0) func NewRootModule(ctx context.Context, dir string) (RootModule, error) { - rm := newRootModule(ctx, dir) + rm := newRootModule(dir) d := &discovery.Discovery{} rm.tfDiscoFunc = d.LookPath @@ -61,73 +88,146 @@ func NewRootModule(ctx context.Context, dir string) (RootModule, error) { rm.tfNewExecutor = exec.NewExecutor rm.newSchemaStorage = schema.NewStorage - return rm, rm.init(ctx) -} + err := rm.discoverCaches(ctx, dir) + if err != nil { + return rm, err + } -func (rm *rootModule) SetLogger(logger *log.Logger) { - rm.logger = logger + return rm, rm.load(ctx) } -func (rm *rootModule) init(ctx context.Context) error { - rm.logger.Printf("initing new root module: %s", rm.path) - tf, err := rm.initTfExecutor(rm.path) +func (rm *rootModule) discoverCaches(ctx context.Context, dir string) error { + var errs *multierror.Error + err := rm.discoverPluginCache(dir) if err != nil { - return err + errs = multierror.Append(errs, err) } - version, err := tf.Version() + err = rm.discoverModuleCache(dir) if err != nil { - return err + errs = multierror.Append(errs, err) } - rm.logger.Printf("Terraform version %s found at %s", version, tf.GetExecPath()) + return errs.ErrorOrNil() +} - err = schema.SchemaSupportsTerraform(version) +func (rm *rootModule) discoverPluginCache(dir string) error { + rm.pluginMu.Lock() + defer rm.pluginMu.Unlock() + + lockPaths := pluginLockFilePaths(dir) + lf, err := findFile(lockPaths) if err != nil { - return err + if os.IsNotExist(err) { + rm.logger.Printf("no plugin cache found: %s", err.Error()) + return nil + } + + return fmt.Errorf("unable to calculate hash: %w", err) } + rm.pluginLockFile = lf + return nil +} + +func (rm *rootModule) discoverModuleCache(dir string) error { + rm.moduleMu.Lock() + defer rm.moduleMu.Unlock() - p, err := lang.FindCompatibleParser(version) + lf, err := newFile(moduleManifestFilePath(dir)) if err != nil { - return err - } - p.SetLogger(rm.logger) + if os.IsNotExist(err) { + rm.logger.Printf("no module manifest file found: %s", err.Error()) + return nil + } - ss := rm.newSchemaStorage() + return fmt.Errorf("unable to calculate hash: %w", err) + } + rm.moduleManifestFile = lf + return nil +} - ss.SetLogger(rm.logger) +func (rm *rootModule) SetLogger(logger *log.Logger) { + rm.logger = logger +} - p.SetSchemaReader(ss) +func (rm *rootModule) StartLoading() { + ctx, cancelFunc := context.WithCancel(context.Background()) + rm.cancelLoading = cancelFunc - rm.parser = p - rm.schemaWriter = ss - rm.tfExec = tf - rm.tfVersion = version + go func(ctx context.Context) { + rm.setLoadErr(rm.load(ctx)) + }(ctx) +} - err = rm.initPluginCache(rm.path) - if err != nil { - return fmt.Errorf("plugin initialization failed: %w", err) - } - err = rm.initModuleCache(rm.path) - if err != nil { - return err +func (rm *rootModule) CancelLoading() { + if !rm.IsLoadingDone() && rm.cancelLoading != nil { + rm.cancelLoading() } - return nil + rm.setLoadingState(false) } -func (rm *rootModule) initTfExecutor(dir string) (*exec.Executor, error) { +func (rm *rootModule) load(ctx context.Context) error { + var errs *multierror.Error + defer rm.CancelLoading() + + // reset internal loading state + rm.setLoadingState(true) + + // The following operations have to happen in a particular order + // as they depend on the internal state as mutated by each operation + + err := rm.UpdateModuleManifest(rm.moduleManifestFile) + errs = multierror.Append(errs, err) + + err = rm.discoverTerraformExecutor(ctx) + rm.tfDiscoErr = err + errs = multierror.Append(errs, err) + + err = rm.discoverTerraformVersion(ctx) + rm.tfVersionErr = err + errs = multierror.Append(errs, err) + + err = rm.findCompatibleStateStorage() + errs = multierror.Append(errs, err) + + err = rm.findCompatibleLangParser() + errs = multierror.Append(errs, err) + + err = rm.UpdateSchemaCache(ctx, rm.pluginLockFile) + errs = multierror.Append(errs, err) + + return errs.ErrorOrNil() +} + +func (rm *rootModule) setLoadingState(isLoading bool) { + rm.isLoadingMu.Lock() + defer rm.isLoadingMu.Unlock() + rm.isLoading = isLoading +} + +func (rm *rootModule) IsLoadingDone() bool { + rm.isLoadingMu.RLock() + defer rm.isLoadingMu.RUnlock() + return !rm.isLoading +} + +func (rm *rootModule) discoverTerraformExecutor(ctx context.Context) error { + defer func() { + rm.setTfLoaded(true) + }() + tfPath := rm.tfExecPath if tfPath == "" { var err error tfPath, err = rm.tfDiscoFunc() if err != nil { - return nil, err + return err } } - tf := rm.tfNewExecutor(rm.ctx, tfPath) + tf := rm.tfNewExecutor(tfPath) - tf.SetWorkdir(dir) + tf.SetWorkdir(rm.path) tf.SetLogger(rm.logger) if rm.tfExecLogPath != "" { @@ -138,81 +238,74 @@ func (rm *rootModule) initTfExecutor(dir string) (*exec.Executor, error) { tf.SetTimeout(rm.tfExecTimeout) } - return tf, nil -} + rm.tfExec = tf -func (rm *rootModule) initPluginCache(dir string) error { - var lf File - if rm.ignorePluginCache { - lf = &file{ - path: pluginLockFilePaths(dir)[0], - } - } else { - var err error - lockPaths := pluginLockFilePaths(dir) - lf, err = findFile(lockPaths) - if err != nil { - if os.IsNotExist(err) { - rm.logger.Printf("no plugin cache found: %s", err.Error()) - return nil - } + return nil +} - return fmt.Errorf("unable to calculate hash: %w", err) - } +func (rm *rootModule) discoverTerraformVersion(ctx context.Context) error { + if rm.tfExec == nil { + return errors.New("no terraform executor - unable to read version") } - return rm.UpdatePluginCache(lf) + version, err := rm.tfExec.Version(ctx) + if err != nil { + return err + } + rm.logger.Printf("Terraform version %s found at %s for %s", version, + rm.tfExec.GetExecPath(), rm.Path()) + rm.tfVersion = version + return nil } -func findFile(paths []string) (File, error) { - var lf File - var err error - - for _, path := range paths { - lf, err = newFile(path) - if err == nil { - return lf, nil - } - if !os.IsNotExist(err) { - return nil, err - } +func (rm *rootModule) findCompatibleStateStorage() error { + if rm.tfVersion == "" { + return errors.New("unknown terraform version - unable to find state storage") } - return nil, err + err := schema.SchemaSupportsTerraform(rm.tfVersion) + if err != nil { + return err + } + rm.schemaStorage = rm.newSchemaStorage() + rm.schemaStorage.SetLogger(rm.logger) + return nil } -type file struct { - path string -} +func (rm *rootModule) findCompatibleLangParser() error { + defer func() { + rm.setParserLoaded(true) + }() -func (f *file) Path() string { - return f.path -} + if rm.tfVersion == "" { + return errors.New("unknown terraform version - unable to find parser") + } -func newFile(path string) (File, error) { - fi, err := os.Stat(path) + p, err := lang.FindCompatibleParser(rm.tfVersion) if err != nil { - return nil, err + return err } - if fi.IsDir() { - return nil, fmt.Errorf("expected %s to be a file, not a dir", path) + p.SetLogger(rm.logger) + + if rm.schemaStorage != nil { + p.SetSchemaReader(rm.schemaStorage) } - return &file{path: path}, nil -} + rm.parser = p -func (rm *rootModule) initModuleCache(dir string) error { - lf, err := newFile(moduleManifestFilePath(dir)) - if err != nil { - if os.IsNotExist(err) { - rm.logger.Printf("no module manifest file found: %s", err.Error()) - return nil - } + return nil +} - return fmt.Errorf("unable to calculate hash: %w", err) - } +func (rm *rootModule) LoadError() error { + rm.loadErrMu.RLock() + defer rm.loadErrMu.RUnlock() + return rm.loadErr +} - return rm.UpdateModuleManifest(lf) +func (rm *rootModule) setLoadErr(err error) { + rm.loadErrMu.Lock() + defer rm.loadErrMu.Unlock() + rm.loadErr = err } func (rm *rootModule) Path() string { @@ -221,9 +314,13 @@ func (rm *rootModule) Path() string { func (rm *rootModule) UpdateModuleManifest(lockFile File) error { rm.moduleMu.Lock() - rm.logger.Printf("updating module manifest based on %s ...", lockFile.Path()) defer rm.moduleMu.Unlock() + if lockFile == nil { + rm.logger.Printf("ignoring module update as no lock file was found for %s", rm.Path()) + return nil + } + rm.moduleManifestFile = lockFile mm, err := ParseModuleManifestFromFile(lockFile.Path()) @@ -232,15 +329,48 @@ func (rm *rootModule) UpdateModuleManifest(lockFile File) error { } rm.moduleManifest = mm - rm.logger.Printf("updated module manifest - %d references parsed", len(mm.Records)) + rm.logger.Printf("updated module manifest - %d references parsed for %s", + len(mm.Records), rm.Path()) return nil } -func (rm *rootModule) Parser() lang.Parser { +func (rm *rootModule) Parser() (lang.Parser, error) { rm.pluginMu.RLock() defer rm.pluginMu.RUnlock() - return rm.parser + if !rm.IsParserLoaded() { + return nil, fmt.Errorf("parser is not loaded yet") + } + + if rm.parser == nil { + return nil, fmt.Errorf("no parser available") + } + + return rm.parser, nil +} + +func (rm *rootModule) IsParserLoaded() bool { + rm.parserLoadedMu.RLock() + defer rm.parserLoadedMu.RUnlock() + return rm.parserLoaded +} + +func (rm *rootModule) setParserLoaded(isLoaded bool) { + rm.parserLoadedMu.Lock() + defer rm.parserLoadedMu.Unlock() + rm.parserLoaded = isLoaded +} + +func (rm *rootModule) IsSchemaLoaded() bool { + rm.schemaLoadedMu.RLock() + defer rm.schemaLoadedMu.RUnlock() + return rm.schemaLoaded +} + +func (rm *rootModule) setSchemaLoaded(isLoaded bool) { + rm.schemaLoadedMu.Lock() + defer rm.schemaLoadedMu.Unlock() + rm.schemaLoaded = isLoaded } func (rm *rootModule) ReferencesModulePath(path string) bool { @@ -267,22 +397,60 @@ func (rm *rootModule) ReferencesModulePath(path string) bool { return false } -func (rm *rootModule) TerraformExecutor() *exec.Executor { - return rm.tfExec +func (rm *rootModule) TerraformExecutor() (*exec.Executor, error) { + if !rm.IsTerraformLoaded() { + return nil, fmt.Errorf("terraform executor is not loaded yet") + } + + if rm.tfExec == nil { + return nil, fmt.Errorf("no terraform executor available") + } + + return rm.tfExec, nil +} + +func (rm *rootModule) IsTerraformLoaded() bool { + rm.tfLoadedMu.RLock() + defer rm.tfLoadedMu.RUnlock() + return rm.tfLoaded +} + +func (rm *rootModule) setTfLoaded(isLoaded bool) { + rm.tfLoadedMu.Lock() + defer rm.tfLoadedMu.Unlock() + rm.tfLoaded = isLoaded } -func (rm *rootModule) UpdatePluginCache(lockFile File) error { +func (rm *rootModule) UpdateSchemaCache(ctx context.Context, lockFile File) error { rm.pluginMu.Lock() defer rm.pluginMu.Unlock() + if !rm.IsTerraformLoaded() { + return fmt.Errorf("cannot update schema as terraform executor is not available yet") + } + + defer func() { + rm.setSchemaLoaded(true) + }() + + if lockFile == nil { + rm.logger.Printf("ignoring schema cache update as no lock file was found for %s", + rm.Path()) + return nil + } + + if rm.schemaStorage == nil { + return fmt.Errorf("cannot update schema as schema cache is not available") + } + rm.pluginLockFile = lockFile - err := rm.schemaWriter.ObtainSchemasForModule( + err := rm.schemaStorage.ObtainSchemasForModule(ctx, rm.tfExec, rootModuleDirFromFilePath(lockFile.Path())) if err != nil { // We fail silently here to still allow tracking the module // The schema can be loaded later via watcher - rm.logger.Printf("failed to update plugin cache: %s", err.Error()) + rm.logger.Printf("failed to update plugin cache for %s: %s", rm.Path(), err.Error()) } return nil diff --git a/internal/terraform/rootmodule/root_module_manager.go b/internal/terraform/rootmodule/root_module_manager.go index 7a9da8780..246c5a7af 100644 --- a/internal/terraform/rootmodule/root_module_manager.go +++ b/internal/terraform/rootmodule/root_module_manager.go @@ -17,29 +17,39 @@ import ( type rootModuleManager struct { rms []*rootModule + newRootModule RootModuleFactory + + syncLoading bool + logger *log.Logger + + // terraform discovery + tfDiscoFunc discovery.DiscoveryFunc + + // terraform executor + tfNewExecutor exec.ExecutorFactory tfExecPath string tfExecTimeout time.Duration tfExecLogPath string - logger *log.Logger - - newRootModule RootModuleFactory } -func NewRootModuleManager(ctx context.Context) RootModuleManager { - return newRootModuleManager(ctx) +func NewRootModuleManager() RootModuleManager { + return newRootModuleManager() } -func newRootModuleManager(ctx context.Context) *rootModuleManager { +func newRootModuleManager() *rootModuleManager { + d := &discovery.Discovery{} rmm := &rootModuleManager{ - rms: make([]*rootModule, 0), - logger: defaultLogger, + rms: make([]*rootModule, 0), + logger: defaultLogger, + tfDiscoFunc: d.LookPath, + tfNewExecutor: exec.NewExecutor, } rmm.newRootModule = rmm.defaultRootModuleFactory return rmm } func (rmm *rootModuleManager) defaultRootModuleFactory(ctx context.Context, dir string) (*rootModule, error) { - rm := newRootModule(ctx, dir) + rm := newRootModule(dir) rm.SetLogger(rmm.logger) @@ -52,7 +62,7 @@ func (rmm *rootModuleManager) defaultRootModuleFactory(ctx context.Context, dir rm.tfExecTimeout = rmm.tfExecTimeout rm.tfExecLogPath = rmm.tfExecLogPath - return rm, rm.init(ctx) + return rm, rm.discoverCaches(ctx, dir) } func (rmm *rootModuleManager) SetTerraformExecPath(path string) { @@ -71,12 +81,12 @@ func (rmm *rootModuleManager) SetLogger(logger *log.Logger) { rmm.logger = logger } -func (rmm *rootModuleManager) AddRootModule(dir string) (RootModule, error) { +func (rmm *rootModuleManager) AddAndStartLoadingRootModule(ctx context.Context, dir string) (RootModule, error) { dir = filepath.Clean(dir) // TODO: Follow symlinks (requires proper test data) - if rmm.exists(dir) { + if _, ok := rmm.rootModuleByPath(dir); ok { return nil, fmt.Errorf("root module %s was already added", dir) } @@ -87,49 +97,48 @@ func (rmm *rootModuleManager) AddRootModule(dir string) (RootModule, error) { rmm.rms = append(rmm.rms, rm) - return rm, nil -} - -func (rmm *rootModuleManager) exists(dir string) bool { - for _, rm := range rmm.rms { - if pathEquals(rm.Path(), dir) { - return true - } + if rmm.syncLoading { + rmm.logger.Printf("synchronously loading root module %s", dir) + return rm, rm.load(ctx) } - return false + + rmm.logger.Printf("asynchronously loading root module %s", dir) + rm.StartLoading() + + return rm, nil } -func (rmm *rootModuleManager) rootModuleByPath(dir string) *rootModule { +func (rmm *rootModuleManager) rootModuleByPath(dir string) (*rootModule, bool) { for _, rm := range rmm.rms { if pathEquals(rm.Path(), dir) { - return rm + return rm, true } } - return nil + return nil, false } -func (rmm *rootModuleManager) RootModuleCandidatesByPath(path string) []string { +func (rmm *rootModuleManager) RootModuleCandidatesByPath(path string) RootModules { path = filepath.Clean(path) // TODO: Follow symlinks (requires proper test data) - if rmm.exists(path) { + if rm, ok := rmm.rootModuleByPath(path); ok { rmm.logger.Printf("direct root module lookup succeeded: %s", path) - return []string{path} + return []RootModule{rm} } dir := rootModuleDirFromFilePath(path) - if rmm.exists(dir) { + if rm, ok := rmm.rootModuleByPath(dir); ok { rmm.logger.Printf("dir-based root module lookup succeeded: %s", dir) - return []string{dir} + return []RootModule{rm} } - candidates := make([]string, 0) + candidates := make([]RootModule, 0) for _, rm := range rmm.rms { rmm.logger.Printf("looking up %s in module references of %s", dir, rm.Path()) if rm.ReferencesModulePath(dir) { rmm.logger.Printf("module-ref-based root module lookup succeeded: %s", dir) - candidates = append(candidates, rm.Path()) + candidates = append(candidates, rm) } } @@ -139,12 +148,7 @@ func (rmm *rootModuleManager) RootModuleCandidatesByPath(path string) []string { func (rmm *rootModuleManager) RootModuleByPath(path string) (RootModule, error) { candidates := rmm.RootModuleCandidatesByPath(path) if len(candidates) > 0 { - firstMatch := candidates[0] - if !rmm.exists(firstMatch) { - return nil, fmt.Errorf("Discovered root module %s not available,"+ - " this is most likely a bug, please report it", firstMatch) - } - return rmm.rootModuleByPath(firstMatch), nil + return candidates[0], nil } return nil, &RootModuleNotFoundErr{path} @@ -156,16 +160,74 @@ func (rmm *rootModuleManager) ParserForDir(path string) (lang.Parser, error) { return nil, err } - return rm.Parser(), nil + return rm.Parser() +} + +func (rmm *rootModuleManager) IsParserLoaded(path string) (bool, error) { + rm, err := rmm.RootModuleByPath(path) + if err != nil { + return false, err + } + + return rm.IsParserLoaded(), nil +} + +func (rmm *rootModuleManager) IsSchemaLoaded(path string) (bool, error) { + rm, err := rmm.RootModuleByPath(path) + if err != nil { + return false, err + } + + return rm.IsSchemaLoaded(), nil } func (rmm *rootModuleManager) TerraformExecutorForDir(ctx context.Context, path string) (*exec.Executor, error) { rm, err := rmm.RootModuleByPath(path) - if err != nil && IsRootModuleNotFound(err) { - return rmm.terraformExecutorForDir(ctx, path) + if err != nil { + if IsRootModuleNotFound(err) { + // TODO: obtain TF version and return "formatter" instead of executor + // gated by particular version which introduced "fmt" + return rmm.discoverTerraformExecutor(ctx, path) + } + return nil, err + } + + return rm.TerraformExecutor() +} + +func (rmm *rootModuleManager) discoverTerraformExecutor(ctx context.Context, path string) (*exec.Executor, error) { + tfPath := rmm.tfExecPath + if tfPath == "" { + var err error + tfPath, err = rmm.tfDiscoFunc() + if err != nil { + return nil, err + } + } + + tf := rmm.tfNewExecutor(tfPath) + + tf.SetWorkdir(path) + tf.SetLogger(rmm.logger) + + if rmm.tfExecLogPath != "" { + tf.SetExecLogPath(rmm.tfExecLogPath) + } + + if rmm.tfExecTimeout != 0 { + tf.SetTimeout(rmm.tfExecTimeout) + } + + return tf, nil +} + +func (rmm *rootModuleManager) IsTerraformLoaded(path string) (bool, error) { + rm, err := rmm.RootModuleByPath(path) + if err != nil { + return false, err } - return rm.TerraformExecutor(), nil + return rm.IsTerraformLoaded(), nil } func (rmm *rootModuleManager) terraformExecutorForDir(ctx context.Context, dir string) (*exec.Executor, error) { @@ -179,7 +241,7 @@ func (rmm *rootModuleManager) terraformExecutorForDir(ctx context.Context, dir s } } - tf := exec.NewExecutor(ctx, tfPath) + tf := exec.NewExecutor(tfPath) tf.SetWorkdir(dir) tf.SetLogger(rmm.logger) @@ -195,6 +257,14 @@ func (rmm *rootModuleManager) terraformExecutorForDir(ctx context.Context, dir s return tf, nil } +func (rmm *rootModuleManager) CancelLoading() { + for _, rm := range rmm.rms { + rmm.logger.Printf("cancelling loading for %s", rm.Path()) + rm.CancelLoading() + rmm.logger.Printf("loading cancelled for %s", rm.Path()) + } +} + // rootModuleDirFromPath strips known lock file paths and filenames // to get the directory path of the relevant rootModule func rootModuleDirFromFilePath(filePath string) string { diff --git a/internal/terraform/rootmodule/root_module_manager_mock.go b/internal/terraform/rootmodule/root_module_manager_mock.go index b8531ba51..94380fd06 100644 --- a/internal/terraform/rootmodule/root_module_manager_mock.go +++ b/internal/terraform/rootmodule/root_module_manager_mock.go @@ -5,17 +5,10 @@ import ( "fmt" "log" - tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-ls/internal/terraform/discovery" "github.com/hashicorp/terraform-ls/internal/terraform/exec" - "github.com/hashicorp/terraform-ls/internal/terraform/schema" ) -type RootModuleMock struct { - TerraformExecQueue exec.MockItemDispenser - ProviderSchemas *tfjson.ProviderSchemas -} - type RootModuleMockFactory struct { rmm map[string]*RootModuleMock logger *log.Logger @@ -27,37 +20,41 @@ func (rmf *RootModuleMockFactory) New(ctx context.Context, dir string) (*rootMod return nil, fmt.Errorf("unexpected root module requested: %s (%d available: %#v)", dir, len(rmf.rmm), rmf.rmm) } - mock := NewRootModuleMock(ctx, rmm, dir) + mock := NewRootModuleMock(rmm, dir) mock.SetLogger(rmf.logger) - return mock, mock.init(ctx) + return mock, mock.discoverCaches(ctx, dir) } -func NewRootModuleMock(ctx context.Context, rmm *RootModuleMock, dir string) *rootModule { - rm := newRootModule(ctx, dir) +type RootModuleManagerMockInput struct { + RootModules map[string]*RootModuleMock + TerraformExecQueue exec.MockItemDispenser +} - md := &discovery.MockDiscovery{Path: "tf-mock"} - rm.tfDiscoFunc = md.LookPath +func NewRootModuleManagerMock(input *RootModuleManagerMockInput) RootModuleManagerFactory { + rmm := newRootModuleManager() + rmm.syncLoading = true - // For now, until we have better testing strategy to mimic real lock files - rm.ignorePluginCache = true + rmf := &RootModuleMockFactory{ + rmm: make(map[string]*RootModuleMock, 0), + logger: rmm.logger, + } - rm.tfNewExecutor = exec.MockExecutor(rmm.TerraformExecQueue) + // mock terraform discovery + md := &discovery.MockDiscovery{Path: "tf-mock"} + rmm.tfDiscoFunc = md.LookPath - if rmm.ProviderSchemas == nil { - rm.newSchemaStorage = schema.NewStorage - } else { - rm.newSchemaStorage = schema.MockStorage(rmm.ProviderSchemas) - } + // mock terraform executor + if input != nil { + rmm.tfNewExecutor = exec.MockExecutor(input.TerraformExecQueue) - return rm -} + if input.RootModules != nil { + rmf.rmm = input.RootModules + } + } -func NewRootModuleManagerMock(m map[string]*RootModuleMock) RootModuleManagerFactory { - rm := newRootModuleManager(context.Background()) - rmf := &RootModuleMockFactory{rmm: m, logger: rm.logger} - rm.newRootModule = rmf.New + rmm.newRootModule = rmf.New - return func(ctx context.Context) RootModuleManager { - return rm + return func() RootModuleManager { + return rmm } } diff --git a/internal/terraform/rootmodule/root_module_manager_mock_test.go b/internal/terraform/rootmodule/root_module_manager_mock_test.go index 0b6f4951b..aff0b352d 100644 --- a/internal/terraform/rootmodule/root_module_manager_mock_test.go +++ b/internal/terraform/rootmodule/root_module_manager_mock_test.go @@ -10,9 +10,9 @@ import ( ) func TestNewRootModuleManagerMock_noMocks(t *testing.T) { - f := NewRootModuleManagerMock(map[string]*RootModuleMock{}) - rmm := f(context.Background()) - _, err := rmm.AddRootModule("any-path") + f := NewRootModuleManagerMock(nil) + rmm := f() + _, err := rmm.AddAndStartLoadingRootModule(context.Background(), "any-path") if err == nil { t.Fatal("expected unmocked path addition to fail") } @@ -21,24 +21,25 @@ func TestNewRootModuleManagerMock_noMocks(t *testing.T) { func TestNewRootModuleManagerMock_mocks(t *testing.T) { tmpDir := filepath.Clean(os.TempDir()) - f := NewRootModuleManagerMock(map[string]*RootModuleMock{ - tmpDir: { - TerraformExecQueue: &exec.MockQueue{ - Q: []*exec.MockItem{ - { - Args: []string{"version"}, - Stdout: "Terraform v0.12.0\n", - }, - { - Args: []string{"providers", "schema", "-json"}, - Stdout: "{\"format_version\":\"0.1\"}\n", + f := NewRootModuleManagerMock(&RootModuleManagerMockInput{ + RootModules: map[string]*RootModuleMock{ + tmpDir: { + TerraformExecQueue: &exec.MockQueue{ + Q: []*exec.MockItem{ + { + Args: []string{"version"}, + Stdout: "Terraform v0.12.0\n", + }, + { + Args: []string{"providers", "schema", "-json"}, + Stdout: `{"format_version":"0.1"}` + "\n", + }, }, }, }, - }, - }) - rmm := f(context.Background()) - _, err := rmm.AddRootModule(tmpDir) + }}) + rmm := f() + _, err := rmm.AddAndStartLoadingRootModule(context.Background(), tmpDir) if err != nil { t.Fatal(err) } diff --git a/internal/terraform/rootmodule/root_module_manager_test.go b/internal/terraform/rootmodule/root_module_manager_test.go index 15ae78986..5dabb7921 100644 --- a/internal/terraform/rootmodule/root_module_manager_test.go +++ b/internal/terraform/rootmodule/root_module_manager_test.go @@ -14,46 +14,6 @@ import ( "github.com/hashicorp/terraform-ls/internal/terraform/exec" ) -// func TestRootModuleManager_RootModuleByPath_basic(t *testing.T) { -// rmm := testRootModuleManager(t) - -// tmpDir := tempDir(t) -// direct, unrelated, dirbased := newTestRootModule(t, "direct"), newTestRootModule(t, "unrelated"), newTestRootModule(t, "dirbased") -// rmm.rms = map[string]*rootModule{ -// direct.Dir: direct.RootModule, -// unrelated.Dir: unrelated.RootModule, -// dirbased.Dir: dirbased.RootModule, -// } - -// w1, err := rmm.RootModuleByPath(direct.Dir) -// if err != nil { -// t.Fatal(err) -// } -// if direct.RootModule != w1 { -// t.Fatalf("unexpected root module found: %p, expected: %p", w1, direct) -// } - -// lockDirPath := filepath.Join(tmpDir, "dirbased", ".terraform", "plugins") -// lockFilePath := filepath.Join(lockDirPath, "selections.json") -// err = os.MkdirAll(lockDirPath, 0775) -// if err != nil { -// t.Fatal(err) -// } -// f, err := os.Create(lockFilePath) -// if err != nil { -// t.Fatal(err) -// } -// f.Close() - -// w2, err := rmm.RootModuleByPath(lockFilePath) -// if err != nil { -// t.Fatal(err) -// } -// if dirbased.RootModule != w2 { -// t.Fatalf("unexpected root module found: %p, expected: %p", w2, dirbased) -// } -// } - func TestRootModuleManager_RootModuleCandidatesByPath(t *testing.T) { testData, err := filepath.Abs("testdata") if err != nil { @@ -472,11 +432,11 @@ func TestRootModuleManager_RootModuleCandidatesByPath(t *testing.T) { for i, tc := range testCases { base := filepath.Base(tc.walkerRoot) - t.Run(fmt.Sprintf("%s/%d-%s", base, i, tc.name), func(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.WalkInitializedRootModules(tc.walkerRoot, func(rmPath string) error { - _, err := rmm.AddRootModule(rmPath) + err := w.StartWalking(tc.walkerRoot, func(ctx context.Context, rmPath string) error { + _, err := rmm.AddAndStartLoadingRootModule(ctx, rmPath) return err }) if err != nil { @@ -484,7 +444,7 @@ func TestRootModuleManager_RootModuleCandidatesByPath(t *testing.T) { } candidates := rmm.RootModuleCandidatesByPath(tc.lookupPath) - if diff := cmp.Diff(tc.expectedCandidates, candidates); diff != "" { + if diff := cmp.Diff(tc.expectedCandidates, candidates.Paths()); diff != "" { t.Fatalf("candidates don't match: %s", diff) } }) @@ -492,12 +452,22 @@ func TestRootModuleManager_RootModuleCandidatesByPath(t *testing.T) { } func testRootModuleManager(t *testing.T) *rootModuleManager { - rmm := newRootModuleManager(context.Background()) + rmm := newRootModuleManager() + rmm.syncLoading = true rmm.logger = testLogger() rmm.newRootModule = func(ctx context.Context, dir string) (*rootModule, error) { - rm := NewRootModuleMock(ctx, &RootModuleMock{ + rm := NewRootModuleMock(&RootModuleMock{ TerraformExecQueue: &exec.MockQueue{ Q: []*exec.MockItem{ + // TODO: Pass mock items as argument to make testing more accurate + { + Args: []string{"version"}, + Stdout: "Terraform v0.12.0\n", + }, + { + Args: []string{"providers", "schema", "-json"}, + Stdout: "{\"format_version\":\"0.1\"}\n", + }, { Args: []string{"version"}, Stdout: "Terraform v0.12.0\n", @@ -509,9 +479,21 @@ func testRootModuleManager(t *testing.T) *rootModuleManager { }, }, }, dir) + rm.logger = testLogger() md := &discovery.MockDiscovery{Path: "tf-mock"} rm.tfDiscoFunc = md.LookPath - return rm, rm.init(ctx) + + err := rm.discoverCaches(ctx, dir) + if err != nil { + t.Fatal(err) + } + + err = rm.load(ctx) + if err != nil { + t.Fatal(err) + } + + return rm, nil } return rmm } diff --git a/internal/terraform/rootmodule/root_module_mock.go b/internal/terraform/rootmodule/root_module_mock.go new file mode 100644 index 000000000..4d1ae9e0a --- /dev/null +++ b/internal/terraform/rootmodule/root_module_mock.go @@ -0,0 +1,32 @@ +package rootmodule + +import ( + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-ls/internal/terraform/discovery" + "github.com/hashicorp/terraform-ls/internal/terraform/exec" + "github.com/hashicorp/terraform-ls/internal/terraform/schema" +) + +type RootModuleMock struct { + TerraformExecQueue exec.MockItemDispenser + ProviderSchemas *tfjson.ProviderSchemas +} + +func NewRootModuleMock(rmm *RootModuleMock, dir string) *rootModule { + rm := newRootModule(dir) + + // mock terraform discovery + md := &discovery.MockDiscovery{Path: "tf-mock"} + rm.tfDiscoFunc = md.LookPath + + // mock terraform executor + rm.tfNewExecutor = exec.MockExecutor(rmm.TerraformExecQueue) + + if rmm.ProviderSchemas == nil { + rm.newSchemaStorage = schema.NewStorage + } else { + rm.newSchemaStorage = schema.MockStorage(rmm.ProviderSchemas) + } + + return rm +} diff --git a/internal/terraform/rootmodule/testdata/main-module-multienv/env/dev/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/main-module-multienv/env/dev/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..51a6f9448 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/main-module-multienv/env/dev/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/main-module-multienv/env/dev/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/main-module-multienv/env/dev/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..51a6f9448 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/main-module-multienv/env/dev/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/main-module-multienv/env/prod/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/main-module-multienv/env/prod/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..51a6f9448 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/main-module-multienv/env/prod/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/main-module-multienv/env/prod/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/main-module-multienv/env/prod/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..51a6f9448 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/main-module-multienv/env/prod/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/main-module-multienv/env/staging/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/main-module-multienv/env/staging/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..51a6f9448 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/main-module-multienv/env/staging/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/main-module-multienv/env/staging/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/main-module-multienv/env/staging/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..51a6f9448 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/main-module-multienv/env/staging/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/first-root/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/first-root/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..ded8dddf6 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/first-root/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,4 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6", + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/first-root/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/first-root/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..ded8dddf6 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/first-root/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,4 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6", + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/second-root/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/second-root/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..ded8dddf6 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/second-root/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,4 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6", + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/second-root/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/second-root/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..ded8dddf6 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/second-root/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,4 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6", + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/third-root/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/third-root/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..ded8dddf6 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/third-root/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,4 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6", + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/third-root/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/third-root/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..ded8dddf6 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-local-modules-down/third-root/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,4 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6", + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/first/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/first/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..894be1e76 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/first/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/first/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/first/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..894be1e76 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/first/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/second/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/second/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..894be1e76 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/second/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/second/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/second/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..894be1e76 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/second/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/third/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/third/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..894be1e76 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/third/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/third/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/third/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..894be1e76 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-local-modules-up/main-module/modules/third/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-no-modules/first-root/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-no-modules/first-root/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..894be1e76 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-no-modules/first-root/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-no-modules/first-root/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-no-modules/first-root/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..894be1e76 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-no-modules/first-root/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-no-modules/second-root/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-no-modules/second-root/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..33307f702 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-no-modules/second-root/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "aws": "15303dfdb1e55005e47559799f5c38f5d8bbca517db42898172c9d637d5b8113" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-no-modules/second-root/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-no-modules/second-root/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..33307f702 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-no-modules/second-root/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "aws": "15303dfdb1e55005e47559799f5c38f5d8bbca517db42898172c9d637d5b8113" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-no-modules/third-root/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-no-modules/third-root/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..3d2cdfc40 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-no-modules/third-root/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "github": "aca175fc74182f1b7c9bfeb40a411755555d9122c13a0f81ddaea97ce0ca4cfc" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/multi-root-no-modules/third-root/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/multi-root-no-modules/third-root/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..3d2cdfc40 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/multi-root-no-modules/third-root/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "github": "aca175fc74182f1b7c9bfeb40a411755555d9122c13a0f81ddaea97ce0ca4cfc" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/nested-single-root-ext-modules-only/tf-root/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/nested-single-root-ext-modules-only/tf-root/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..51a6f9448 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/nested-single-root-ext-modules-only/tf-root/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/nested-single-root-ext-modules-only/tf-root/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/nested-single-root-ext-modules-only/tf-root/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..51a6f9448 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/nested-single-root-ext-modules-only/tf-root/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/nested-single-root-local-modules-down/tf-root/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/nested-single-root-local-modules-down/tf-root/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..ded8dddf6 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/nested-single-root-local-modules-down/tf-root/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,4 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6", + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/nested-single-root-local-modules-down/tf-root/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/nested-single-root-local-modules-down/tf-root/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..ded8dddf6 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/nested-single-root-local-modules-down/tf-root/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,4 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6", + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/nested-single-root-local-modules-up/module/tf-root/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/nested-single-root-local-modules-up/module/tf-root/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..894be1e76 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/nested-single-root-local-modules-up/module/tf-root/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/nested-single-root-local-modules-up/module/tf-root/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/nested-single-root-local-modules-up/module/tf-root/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..894be1e76 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/nested-single-root-local-modules-up/module/tf-root/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/nested-single-root-no-modules/tf-root/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/nested-single-root-no-modules/tf-root/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..51a6f9448 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/nested-single-root-no-modules/tf-root/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/nested-single-root-no-modules/tf-root/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/nested-single-root-no-modules/tf-root/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..51a6f9448 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/nested-single-root-no-modules/tf-root/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/single-root-ext-modules-only/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/single-root-ext-modules-only/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..43887ca59 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/single-root-ext-modules-only/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,4 @@ +{ + "google": "8a868aee3493785d724d5521a252b28b0763376c50205283cb4e773a612f396b", + "null": "b1d97b7013b6aaa4205bad9db8ce7ff4d6fc27d7c6ed8b2227213f3441f6208e" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/single-root-ext-modules-only/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/single-root-ext-modules-only/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..43887ca59 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/single-root-ext-modules-only/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,4 @@ +{ + "google": "8a868aee3493785d724d5521a252b28b0763376c50205283cb4e773a612f396b", + "null": "b1d97b7013b6aaa4205bad9db8ce7ff4d6fc27d7c6ed8b2227213f3441f6208e" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/single-root-local-and-ext-modules/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/single-root-local-and-ext-modules/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..4a906973c --- /dev/null +++ b/internal/terraform/rootmodule/testdata/single-root-local-and-ext-modules/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,6 @@ +{ + "aws": "15303dfdb1e55005e47559799f5c38f5d8bbca517db42898172c9d637d5b8113", + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6", + "google": "8a868aee3493785d724d5521a252b28b0763376c50205283cb4e773a612f396b", + "null": "b1d97b7013b6aaa4205bad9db8ce7ff4d6fc27d7c6ed8b2227213f3441f6208e" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/single-root-local-and-ext-modules/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/single-root-local-and-ext-modules/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..4a906973c --- /dev/null +++ b/internal/terraform/rootmodule/testdata/single-root-local-and-ext-modules/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,6 @@ +{ + "aws": "15303dfdb1e55005e47559799f5c38f5d8bbca517db42898172c9d637d5b8113", + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6", + "google": "8a868aee3493785d724d5521a252b28b0763376c50205283cb4e773a612f396b", + "null": "b1d97b7013b6aaa4205bad9db8ce7ff4d6fc27d7c6ed8b2227213f3441f6208e" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/single-root-local-modules-only/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/single-root-local-modules-only/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..301444681 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/single-root-local-modules-only/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,4 @@ +{ + "aws": "15303dfdb1e55005e47559799f5c38f5d8bbca517db42898172c9d637d5b8113", + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/single-root-local-modules-only/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/single-root-local-modules-only/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..301444681 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/single-root-local-modules-only/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,4 @@ +{ + "aws": "15303dfdb1e55005e47559799f5c38f5d8bbca517db42898172c9d637d5b8113", + "azurerm": "718d753146a7589a552a7586dde44e24c12a1719b8122ecca1e244d861d7fca6" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/single-root-no-modules/.terraform/plugins/linux_amd64/lock.json b/internal/terraform/rootmodule/testdata/single-root-no-modules/.terraform/plugins/linux_amd64/lock.json new file mode 100755 index 000000000..51a6f9448 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/single-root-no-modules/.terraform/plugins/linux_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/testdata/single-root-no-modules/.terraform/plugins/windows_amd64/lock.json b/internal/terraform/rootmodule/testdata/single-root-no-modules/.terraform/plugins/windows_amd64/lock.json new file mode 100755 index 000000000..51a6f9448 --- /dev/null +++ b/internal/terraform/rootmodule/testdata/single-root-no-modules/.terraform/plugins/windows_amd64/lock.json @@ -0,0 +1,3 @@ +{ + "random": "7903b3f4d7067b3e8ca2440aa4342b57286310e074a806d0f1a673034969817b" +} \ No newline at end of file diff --git a/internal/terraform/rootmodule/types.go b/internal/terraform/rootmodule/types.go index 442472f0f..c2bc529d7 100644 --- a/internal/terraform/rootmodule/types.go +++ b/internal/terraform/rootmodule/types.go @@ -15,14 +15,17 @@ type File interface { type ParserFinder interface { ParserForDir(path string) (lang.Parser, error) + IsParserLoaded(path string) (bool, error) + IsSchemaLoaded(path string) (bool, error) } type TerraformExecFinder interface { TerraformExecutorForDir(ctx context.Context, path string) (*exec.Executor, error) + IsTerraformLoaded(path string) (bool, error) } type RootModuleCandidateFinder interface { - RootModuleCandidatesByPath(path string) []string + RootModuleCandidatesByPath(path string) RootModules } type RootModuleManager interface { @@ -31,26 +34,46 @@ type RootModuleManager interface { RootModuleCandidateFinder SetLogger(logger *log.Logger) + SetTerraformExecPath(path string) SetTerraformExecLogPath(logPath string) SetTerraformExecTimeout(timeout time.Duration) - AddRootModule(dir string) (RootModule, error) + + AddAndStartLoadingRootModule(ctx context.Context, dir string) (RootModule, error) PathsToWatch() []string RootModuleByPath(path string) (RootModule, error) + CancelLoading() +} + +type RootModules []RootModule + +func (rms RootModules) Paths() []string { + paths := make([]string, len(rms)) + for i, rm := range rms { + paths[i] = rm.Path() + } + return paths } type RootModule interface { + Path() string + LoadError() error + StartLoading() + IsLoadingDone() bool IsKnownPluginLockFile(path string) bool IsKnownModuleManifestFile(path string) bool PathsToWatch() []string - UpdatePluginCache(lockFile File) error + UpdateSchemaCache(ctx context.Context, lockFile File) error + IsSchemaLoaded() bool UpdateModuleManifest(manifestFile File) error - Parser() lang.Parser - TerraformExecutor() *exec.Executor + Parser() (lang.Parser, error) + IsParserLoaded() bool + TerraformExecutor() (*exec.Executor, error) + IsTerraformLoaded() bool } type RootModuleFactory func(context.Context, string) (*rootModule, error) -type RootModuleManagerFactory func(context.Context) RootModuleManager +type RootModuleManagerFactory func() RootModuleManager type WalkerFactory func() *Walker diff --git a/internal/terraform/rootmodule/walker.go b/internal/terraform/rootmodule/walker.go index 07737cf03..2c25d9ea3 100644 --- a/internal/terraform/rootmodule/walker.go +++ b/internal/terraform/rootmodule/walker.go @@ -1,6 +1,7 @@ package rootmodule import ( + "context" "fmt" "io/ioutil" "log" @@ -25,7 +26,9 @@ type Walker struct { logger *log.Logger sync bool walking bool - doneCh chan struct{} + + cancelFunc context.CancelFunc + doneCh chan struct{} } func NewWalker() *Walker { @@ -39,28 +42,36 @@ func (w *Walker) SetLogger(logger *log.Logger) { w.logger = logger } -type WalkFunc func(rootModulePath string) error +type WalkFunc func(ctx context.Context, rootModulePath string) error func (w *Walker) Stop() { + if w.cancelFunc != nil { + w.cancelFunc() + } + if w.walking { w.walking = false w.doneCh <- struct{}{} } } -func (w *Walker) WalkInitializedRootModules(path string, wf WalkFunc) error { +func (w *Walker) StartWalking(path string, wf WalkFunc) error { if w.walking { return fmt.Errorf("walker is already running") } + + ctx, cancelFunc := context.WithCancel(context.Background()) + w.cancelFunc = cancelFunc w.walking = true + if w.sync { w.logger.Printf("synchronously walking through %s", path) - return w.walk(path, wf) + return w.walk(ctx, path, wf) } go func(w *Walker, path string, wf WalkFunc) { w.logger.Printf("asynchronously walking through %s", path) - err := w.walk(path, wf) + err := w.walk(ctx, path, wf) if err != nil { w.logger.Printf("async walking through %s failed: %s", path, err) return @@ -71,7 +82,11 @@ func (w *Walker) WalkInitializedRootModules(path string, wf WalkFunc) error { return nil } -func (w *Walker) walk(rootPath string, wf WalkFunc) error { +func (w *Walker) IsWalking() bool { + return w.walking +} + +func (w *Walker) walk(ctx context.Context, rootPath string, wf WalkFunc) error { err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error { select { case <-w.doneCh: @@ -92,7 +107,7 @@ func (w *Walker) walk(rootPath string, wf WalkFunc) error { } w.logger.Printf("found root module %s", rootDir) - return wf(rootDir) + return wf(ctx, rootDir) } if !info.IsDir() { @@ -107,6 +122,7 @@ func (w *Walker) walk(rootPath string, wf WalkFunc) error { return nil }) + w.logger.Printf("walking of %s finished", rootPath) w.walking = false return err } diff --git a/internal/terraform/schema/schema_storage.go b/internal/terraform/schema/schema_storage.go index eae99cbc0..4f16ff1c0 100644 --- a/internal/terraform/schema/schema_storage.go +++ b/internal/terraform/schema/schema_storage.go @@ -24,7 +24,7 @@ type Reader interface { } type Writer interface { - ObtainSchemasForModule(*exec.Executor, string) error + ObtainSchemasForModule(context.Context, *exec.Executor, string) error } type Resource struct { @@ -62,6 +62,11 @@ func NewStorage() *Storage { } } +func NewStorageForVersion(version string) (*Storage, error) { + // TODO: https://github.com/hashicorp/terraform-ls/issues/164 + return nil, fmt.Errorf("not implemented yet") +} + func SchemaSupportsTerraform(v string) error { c, err := version.NewConstraint( ">= 0.12.0", // Version 0.12 first introduced machine-readable schemas @@ -99,13 +104,13 @@ func (s *Storage) SetLogger(logger *log.Logger) { s.logger = logger } -// ObtainSchemasForModule will (by default) asynchronously obtain schema via tf +// ObtainSchemasForModule will obtain schema via tf // and store it for later consumption via Reader methods -func (s *Storage) ObtainSchemasForModule(tf *exec.Executor, dir string) error { - return s.obtainSchemasForModule(tf, dir) +func (s *Storage) ObtainSchemasForModule(ctx context.Context, tf *exec.Executor, dir string) error { + return s.obtainSchemasForModule(ctx, tf, dir) } -func (s *Storage) obtainSchemasForModule(tf *exec.Executor, dir string) error { +func (s *Storage) obtainSchemasForModule(ctx context.Context, tf *exec.Executor, dir string) error { s.logger.Printf("Acquiring semaphore before retrieving schema for %q ...", dir) err := s.sem.Acquire(context.Background(), 1) if err != nil { @@ -117,7 +122,7 @@ func (s *Storage) obtainSchemasForModule(tf *exec.Executor, dir string) error { s.logger.Printf("Retrieving schemas for %q ...", dir) start := time.Now() - ps, err := tf.ProviderSchemas() + ps, err := tf.ProviderSchemas(ctx) if err != nil { return fmt.Errorf("Unable to retrieve schemas for %q: %w", dir, err) } diff --git a/internal/watcher/types.go b/internal/watcher/types.go index cb6009cab..0555bb23d 100644 --- a/internal/watcher/types.go +++ b/internal/watcher/types.go @@ -1,6 +1,7 @@ package watcher import ( + "context" "log" ) @@ -18,4 +19,4 @@ type Watcher interface { AddChangeHook(f ChangeHook) } -type ChangeHook func(file TrackedFile) error +type ChangeHook func(ctx context.Context, file TrackedFile) error diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index b7d4f8361..252859543 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -1,6 +1,7 @@ package watcher import ( + "context" "io/ioutil" "log" @@ -14,8 +15,10 @@ type watcher struct { fw *fsnotify.Watcher trackedFiles map[string]TrackedFile changeHooks []ChangeHook - watching bool logger *log.Logger + + watching bool + cancelFunc context.CancelFunc } type WatcherFactory func() (Watcher, error) @@ -64,7 +67,7 @@ func (w *watcher) AddChangeHook(h ChangeHook) { w.changeHooks = append(w.changeHooks, h) } -func (w *watcher) run() { +func (w *watcher) run(ctx context.Context) { for { select { case event, ok := <-w.fw.Events: @@ -84,7 +87,7 @@ func (w *watcher) run() { if oldTf.Sha256Sum() != newTf.Sha256Sum() { for _, h := range w.changeHooks { - err := h(newTf) + err := h(ctx, newTf) if err != nil { w.logger.Println("change hook error:", err) } @@ -108,9 +111,12 @@ func (w *watcher) Start() error { return nil } - go w.run() + ctx, cancelFunc := context.WithCancel(context.Background()) + w.cancelFunc = cancelFunc w.watching = true - w.logger.Printf("Watching for changes ...") + + w.logger.Printf("watching for changes ...") + go w.run(ctx) return nil } @@ -120,6 +126,8 @@ func (w *watcher) Stop() error { return nil } + w.cancelFunc() + err := w.fw.Close() if err == nil { w.watching = false diff --git a/langserver/handlers/complete.go b/langserver/handlers/complete.go index f0e150c1d..aae4329fd 100644 --- a/langserver/handlers/complete.go +++ b/langserver/handlers/complete.go @@ -43,6 +43,25 @@ func (h *logHandler) TextDocumentComplete(ctx context.Context, params lsp.Comple pos := fPos.Position() + isParserLoaded, err := pf.IsParserLoaded(file.Dir()) + if err != nil { + return list, err + } + if !isParserLoaded { + // TODO: block until it's available <-pf.ParserLoadingDone() + // requires https://github.com/hashicorp/terraform-ls/issues/8 + return list, fmt.Errorf("parser is not available yet for %s", file.Dir()) + } + + isSchemaLoaded, err := pf.IsSchemaLoaded(file.Dir()) + if err != nil { + return list, err + } + if !isSchemaLoaded { + // TODO: Provide basic completion without schema + return list, fmt.Errorf("schema is not available yet for %s", file.Dir()) + } + p, err := pf.ParserForDir(file.Dir()) if err != nil { return list, fmt.Errorf("finding compatible parser failed: %w", err) diff --git a/langserver/handlers/complete_test.go b/langserver/handlers/complete_test.go index bb9e3ecc7..717f4fb0d 100644 --- a/langserver/handlers/complete_test.go +++ b/langserver/handlers/complete_test.go @@ -30,24 +30,26 @@ func TestCompletion_withoutInitialization(t *testing.T) { func TestCompletion_withValidData(t *testing.T) { tmpDir := TempDir(t) - InitDir(t, tmpDir.Dir()) + t.Logf("will init at %s", tmpDir.Dir()) + InitPluginCache(t, tmpDir.Dir()) - ls := langserver.NewLangServerMock(t, NewMockSession(map[string]*rootmodule.RootModuleMock{ - tmpDir.Dir(): { - TerraformExecQueue: &exec.MockQueue{ - Q: []*exec.MockItem{ - { - Args: []string{"version"}, - Stdout: "Terraform v0.12.0\n", - }, - { - Args: []string{"providers", "schema", "-json"}, - Stdout: testSchemaOutput, + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + RootModules: map[string]*rootmodule.RootModuleMock{ + tmpDir.Dir(): { + TerraformExecQueue: &exec.MockQueue{ + Q: []*exec.MockItem{ + { + Args: []string{"version"}, + Stdout: "Terraform v0.12.0\n", + }, + { + Args: []string{"providers", "schema", "-json"}, + Stdout: testSchemaOutput, + }, }, }, }, - }, - })) + }})) stop := ls.Start(t) defer stop() diff --git a/langserver/handlers/did_open.go b/langserver/handlers/did_open.go index 457324748..060148e85 100644 --- a/langserver/handlers/did_open.go +++ b/langserver/handlers/did_open.go @@ -9,10 +9,11 @@ import ( "github.com/creachadair/jrpc2" lsctx "github.com/hashicorp/terraform-ls/internal/context" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" + "github.com/hashicorp/terraform-ls/internal/terraform/rootmodule" lsp "github.com/sourcegraph/go-lsp" ) -func TextDocumentDidOpen(ctx context.Context, params lsp.DidOpenTextDocumentParams) error { +func (lh *logHandler) TextDocumentDidOpen(ctx context.Context, params lsp.DidOpenTextDocumentParams) error { fs, err := lsctx.Filesystem(ctx) if err != nil { return err @@ -29,10 +30,19 @@ func TextDocumentDidOpen(ctx context.Context, params lsp.DidOpenTextDocumentPara return err } + walker, err := lsctx.RootModuleWalker(ctx) + if err != nil { + return err + } + rootDir, _ := lsctx.RootDirectory(ctx) candidates := cf.RootModuleCandidatesByPath(f.Dir()) - if len(candidates) == 0 { + + if walker.IsWalking() { + // avoid raising false warnings if walker hasn't finished yet + lh.logger.Printf("walker has not finished walking yet, data may be inaccurate for %s", f.FullPath()) + } else if len(candidates) == 0 { msg := fmt.Sprintf("No root module found for %s."+ " Functionality may be limited."+ // Unfortunately we can't be any more specific wrt where @@ -47,8 +57,8 @@ func TextDocumentDidOpen(ctx context.Context, params lsp.DidOpenTextDocumentPara // TODO: Suggest specifying explicit root modules? msg := fmt.Sprintf("Alternative root modules found for %s (%s), picked: %s", - f.Filename(), renderCandidates(rootDir, candidates[1:]), - renderCandidate(rootDir, candidates[0])) + f.Filename(), candidatePaths(rootDir, candidates[1:]), + renderCandidatePath(rootDir, candidates[0])) return jrpc2.ServerPush(ctx, "window/showMessage", lsp.ShowMessageParams{ Type: lsp.MTWarning, Message: msg, @@ -58,17 +68,18 @@ func TextDocumentDidOpen(ctx context.Context, params lsp.DidOpenTextDocumentPara return nil } -func renderCandidates(rootDir string, candidatePaths []string) string { - for i, p := range candidatePaths { +func candidatePaths(rootDir string, candidates []rootmodule.RootModule) string { + paths := make([]string, len(candidates)) + for i, rm := range candidates { // This helps displaying shorter, but still relevant paths - candidatePaths[i] = renderCandidate(rootDir, p) + paths[i] = renderCandidatePath(rootDir, rm) } - return strings.Join(candidatePaths, ", ") + return strings.Join(paths, ", ") } -func renderCandidate(rootDir, path string) string { +func renderCandidatePath(rootDir string, candidate rootmodule.RootModule) string { trimmed := strings.TrimPrefix( - strings.TrimPrefix(path, rootDir), string(os.PathSeparator)) + strings.TrimPrefix(candidate.Path(), rootDir), string(os.PathSeparator)) if trimmed == "" { return "." } diff --git a/langserver/handlers/formatting.go b/langserver/handlers/formatting.go index 7bf838e29..c0c554625 100644 --- a/langserver/handlers/formatting.go +++ b/langserver/handlers/formatting.go @@ -2,10 +2,13 @@ package handlers import ( "context" + "fmt" lsctx "github.com/hashicorp/terraform-ls/internal/context" "github.com/hashicorp/terraform-ls/internal/hcl" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" + "github.com/hashicorp/terraform-ls/internal/terraform/exec" + "github.com/hashicorp/terraform-ls/internal/terraform/rootmodule" lsp "github.com/sourcegraph/go-lsp" ) @@ -28,16 +31,12 @@ func (h *logHandler) TextDocumentFormatting(ctx context.Context, params lsp.Docu return edits, err } - tf, err := tff.TerraformExecutorForDir(ctx, fh.Dir()) + tf, err := findTerraformExecutor(ctx, tff, file.Dir()) if err != nil { - // TODO: detect no root module found error - // -> find OS-wide executor instead return edits, err } - // TODO: This should probably be FormatWithContext() - // so it's cancellable on request cancellation - formatted, err := tf.Format(file.Text()) + formatted, err := tf.Format(ctx, file.Text()) if err != nil { return edits, err } @@ -46,3 +45,20 @@ func (h *logHandler) TextDocumentFormatting(ctx context.Context, params lsp.Docu return ilsp.TextEdits(changes), nil } + +func findTerraformExecutor(ctx context.Context, tff rootmodule.TerraformExecFinder, dir string) (*exec.Executor, error) { + isLoaded, err := tff.IsTerraformLoaded(dir) + if err != nil { + if rootmodule.IsRootModuleNotFound(err) { + return tff.TerraformExecutorForDir(ctx, dir) + } + return nil, err + } else { + if !isLoaded { + // TODO: block until it's available <-tff.TerraformLoadingDone() + return nil, fmt.Errorf("terraform is not available yet for %s", dir) + } + } + + return tff.TerraformExecutorForDir(ctx, dir) +} diff --git a/langserver/handlers/formatting_test.go b/langserver/handlers/formatting_test.go index bf0b1df65..e0d34824d 100644 --- a/langserver/handlers/formatting_test.go +++ b/langserver/handlers/formatting_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/hashicorp/terraform-ls/internal/terraform/exec" - "github.com/hashicorp/terraform-ls/internal/terraform/rootmodule" "github.com/hashicorp/terraform-ls/langserver" "github.com/hashicorp/terraform-ls/langserver/session" ) @@ -28,15 +27,15 @@ func TestLangServer_formattingWithoutInitialization(t *testing.T) { } func TestLangServer_formatting_basic(t *testing.T) { - tmpDir := TempDir(t) - InitDir(t, tmpDir.Dir()) - queue := validTfMockCalls() - queue.Q = append(queue.Q, &exec.MockItem{ - Args: []string{"fmt", "-"}, - Stdout: "provider \"test\" {\n\n}\n", - }) - ls := langserver.NewLangServerMock(t, NewMockSession(map[string]*rootmodule.RootModuleMock{ - tmpDir.Dir(): {TerraformExecQueue: queue}, + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + ManagerTfExecQueue: &exec.MockQueue{ + Q: []*exec.MockItem{ + { + Args: []string{"fmt", "-"}, + Stdout: "provider \"test\" {\n\n}\n", + }, + }, + }, })) stop := ls.Start(t) defer stop() diff --git a/langserver/handlers/handlers_test.go b/langserver/handlers/handlers_test.go index 988d400df..b94cc129f 100644 --- a/langserver/handlers/handlers_test.go +++ b/langserver/handlers/handlers_test.go @@ -14,9 +14,10 @@ import ( ) func TestInitalizeAndShutdown(t *testing.T) { - ls := langserver.NewLangServerMock(t, NewMockSession(map[string]*rootmodule.RootModuleMock{ - TempDir(t).Dir(): {TerraformExecQueue: validTfMockCalls()}, - })) + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + RootModules: map[string]*rootmodule.RootModuleMock{ + TempDir(t).Dir(): {TerraformExecQueue: validTfMockCalls()}, + }})) stop := ls.Start(t) defer stop() @@ -50,9 +51,10 @@ func TestInitalizeAndShutdown(t *testing.T) { } func TestEOF(t *testing.T) { - ms := newMockSession(map[string]*rootmodule.RootModuleMock{ - TempDir(t).Dir(): {TerraformExecQueue: validTfMockCalls()}, - }) + ms := newMockSession(&MockSessionInput{ + RootModules: map[string]*rootmodule.RootModuleMock{ + TempDir(t).Dir(): {TerraformExecQueue: validTfMockCalls()}, + }}) ls := langserver.NewLangServerMock(t, ms.new) stop := ls.Start(t) defer stop() @@ -137,8 +139,17 @@ func TempDir(t *testing.T) lsp.FileHandler { return lsp.FileHandlerFromDirPath(tmpDir) } -func InitDir(t *testing.T, dir string) { - err := os.Mkdir(filepath.Join(dir, ".terraform"), 0755) +func InitPluginCache(t *testing.T, dir string) { + pluginCacheDir := filepath.Join(dir, ".terraform", "plugins") + err := os.MkdirAll(pluginCacheDir, 0755) + if err != nil { + t.Fatal(err) + } + f, err := os.Create(filepath.Join(pluginCacheDir, "selections.json")) + if err != nil { + t.Fatal(err) + } + err = f.Close() if err != nil { t.Fatal(err) } diff --git a/langserver/handlers/initialize.go b/langserver/handlers/initialize.go index 2e82326d6..3cff49a47 100644 --- a/langserver/handlers/initialize.go +++ b/langserver/handlers/initialize.go @@ -60,9 +60,9 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam } walker.SetLogger(lh.logger) - err = walker.WalkInitializedRootModules(fh.Dir(), func(dir string) error { + err = walker.StartWalking(fh.Dir(), func(ctx context.Context, dir string) error { lh.logger.Printf("Adding root module: %s", dir) - rm, err := rmm.AddRootModule(dir) + rm, err := rmm.AddAndStartLoadingRootModule(ctx, dir) if err != nil { return err } diff --git a/langserver/handlers/initialize_test.go b/langserver/handlers/initialize_test.go index 1f0c8851e..87bb6e4b9 100644 --- a/langserver/handlers/initialize_test.go +++ b/langserver/handlers/initialize_test.go @@ -11,9 +11,10 @@ import ( ) func TestInitialize_twice(t *testing.T) { - ls := langserver.NewLangServerMock(t, NewMockSession(map[string]*rootmodule.RootModuleMock{ - TempDir(t).Dir(): {TerraformExecQueue: validTfMockCalls()}, - })) + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + RootModules: map[string]*rootmodule.RootModuleMock{ + TempDir(t).Dir(): {TerraformExecQueue: validTfMockCalls()}, + }})) stop := ls.Start(t) defer stop() @@ -35,31 +36,32 @@ func TestInitialize_twice(t *testing.T) { func TestInitialize_withIncompatibleTerraformVersion(t *testing.T) { tmpDir := TempDir(t) - InitDir(t, tmpDir.Dir()) - ls := langserver.NewLangServerMock(t, NewMockSession(map[string]*rootmodule.RootModuleMock{ - tmpDir.Dir(): { - TerraformExecQueue: &exec.MockCall{ - Args: []string{"version"}, - Stdout: "Terraform v0.11.0\n", + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + RootModules: map[string]*rootmodule.RootModuleMock{ + tmpDir.Dir(): { + TerraformExecQueue: &exec.MockCall{ + Args: []string{"version"}, + Stdout: "Terraform v0.11.0\n", + }, }, - }, - })) + }})) stop := ls.Start(t) defer stop() - ls.CallAndExpectError(t, &langserver.CallRequest{ + ls.Call(t, &langserver.CallRequest{ Method: "initialize", ReqParams: fmt.Sprintf(`{ "capabilities": {}, "processId": 12345, "rootUri": %q - }`, TempDir(t).URI())}, code.SystemError.Err()) + }`, TempDir(t).URI())}) } func TestInitialize_withInvalidRootURI(t *testing.T) { - ls := langserver.NewLangServerMock(t, NewMockSession(map[string]*rootmodule.RootModuleMock{ - TempDir(t).Dir(): {TerraformExecQueue: validTfMockCalls()}, - })) + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + RootModules: map[string]*rootmodule.RootModuleMock{ + TempDir(t).Dir(): {TerraformExecQueue: validTfMockCalls()}, + }})) stop := ls.Start(t) defer stop() diff --git a/langserver/handlers/service.go b/langserver/handlers/service.go index 7d2df8bae..36188ba02 100644 --- a/langserver/handlers/service.go +++ b/langserver/handlers/service.go @@ -28,6 +28,7 @@ type service struct { watcher watcher.Watcher walker *rootmodule.Walker + modMgr rootmodule.RootModuleManager newRootModuleManager rootmodule.RootModuleManagerFactory newWatcher watcher.WatcherFactory newWalker rootmodule.WalkerFactory @@ -69,20 +70,20 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { lh := LogHandler(svc.logger) cc := &lsp.ClientCapabilities{} - rmm := svc.newRootModuleManager(svc.sessCtx) - rmm.SetLogger(svc.logger) + svc.modMgr = svc.newRootModuleManager() + svc.modMgr.SetLogger(svc.logger) svc.walker = svc.newWalker() // The following is set via CLI flags, hence available in the server context if path, ok := lsctx.TerraformExecPath(svc.srvCtx); ok { - rmm.SetTerraformExecPath(path) + svc.modMgr.SetTerraformExecPath(path) } if path, ok := lsctx.TerraformExecLogPath(svc.srvCtx); ok { - rmm.SetTerraformExecLogPath(path) + svc.modMgr.SetTerraformExecLogPath(path) } if timeout, ok := lsctx.TerraformExecTimeout(svc.srvCtx); ok { - rmm.SetTerraformExecTimeout(timeout) + svc.modMgr.SetTerraformExecTimeout(timeout) } ww, err := svc.newWatcher() @@ -91,20 +92,20 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { } svc.watcher = ww svc.watcher.SetLogger(svc.logger) - svc.watcher.AddChangeHook(func(file watcher.TrackedFile) error { - w, err := rmm.RootModuleByPath(file.Path()) + svc.watcher.AddChangeHook(func(ctx context.Context, file watcher.TrackedFile) error { + w, err := svc.modMgr.RootModuleByPath(file.Path()) if err != nil { return err } if w.IsKnownPluginLockFile(file.Path()) { - svc.logger.Printf("detected plugin cache change, updating ...") - return w.UpdatePluginCache(file) + svc.logger.Printf("detected plugin cache change, updating schema ...") + return w.UpdateSchemaCache(ctx, file) } return nil }) - svc.watcher.AddChangeHook(func(file watcher.TrackedFile) error { - rm, err := rmm.RootModuleByPath(file.Path()) + svc.watcher.AddChangeHook(func(_ context.Context, file watcher.TrackedFile) error { + rm, err := svc.modMgr.RootModuleByPath(file.Path()) if err != nil { return err } @@ -133,7 +134,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { ctx = lsctx.WithWatcher(ww, ctx) ctx = lsctx.WithRootModuleWalker(svc.walker, ctx) ctx = lsctx.WithRootDirectory(&rootDir, ctx) - ctx = lsctx.WithRootModuleManager(rmm, ctx) + ctx = lsctx.WithRootModuleManager(svc.modMgr, ctx) return handle(ctx, req, lh.Initialize) }, @@ -161,8 +162,9 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { } ctx = lsctx.WithFilesystem(fs, ctx) ctx = lsctx.WithRootDirectory(&rootDir, ctx) - ctx = lsctx.WithRootModuleCandidateFinder(rmm, ctx) - return handle(ctx, req, TextDocumentDidOpen) + ctx = lsctx.WithRootModuleCandidateFinder(svc.modMgr, ctx) + ctx = lsctx.WithRootModuleWalker(svc.walker, ctx) + return handle(ctx, req, lh.TextDocumentDidOpen) }, "textDocument/didClose": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { err := session.CheckInitializationIsConfirmed() @@ -180,7 +182,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { ctx = lsctx.WithFilesystem(fs, ctx) // TODO: Read-only FS ctx = lsctx.WithClientCapabilities(cc, ctx) - ctx = lsctx.WithParserFinder(rmm, ctx) + ctx = lsctx.WithParserFinder(svc.modMgr, ctx) return handle(ctx, req, lh.TextDocumentComplete) }, @@ -191,7 +193,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { } ctx = lsctx.WithFilesystem(fs, ctx) - ctx = lsctx.WithTerraformExecFinder(rmm, ctx) + ctx = lsctx.WithTerraformExecFinder(svc.modMgr, ctx) return handle(ctx, req, lh.TextDocumentFormatting) }, @@ -201,6 +203,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return nil, err } ctx = lsctx.WithFilesystem(fs, ctx) + svc.shutdown() return handle(ctx, req, Shutdown) }, "exit": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { @@ -231,23 +234,32 @@ func (svc *service) Finish(status jrpc2.ServerStatus) { svc.logger.Printf("session stopped unexpectedly (err: %v)", status.Err) } - // TODO: Cancel any operations on tracked root modules - // https://github.com/hashicorp/terraform-ls/issues/195 + svc.shutdown() + svc.stopSession() +} +func (svc *service) shutdown() { if svc.walker != nil { - svc.logger.Printf("Stopping walker for session ...") + svc.logger.Printf("stopping walker for session ...") svc.walker.Stop() + svc.logger.Printf("walker stopped") } if svc.watcher != nil { - svc.logger.Println("Stopping watcher for session ...") + svc.logger.Println("stopping watcher for session ...") err := svc.watcher.Stop() if err != nil { - svc.logger.Println("Unable to stop watcher for session:", err) + svc.logger.Println("unable to stop watcher for session:", err) + } else { + svc.logger.Println("watcher stopped") } } - svc.stopSession() + if svc.modMgr != nil { + svc.logger.Println("cancelling any root module loading ...") + svc.modMgr.CancelLoading() + svc.logger.Println("root module loading cancelled") + } } // convertMap is a helper function allowing us to omit the jrpc2.Func diff --git a/langserver/handlers/service_mock_test.go b/langserver/handlers/service_mock_test.go index 7543be54f..e03f801ab 100644 --- a/langserver/handlers/service_mock_test.go +++ b/langserver/handlers/service_mock_test.go @@ -7,13 +7,19 @@ import ( "os" "testing" + "github.com/hashicorp/terraform-ls/internal/terraform/exec" "github.com/hashicorp/terraform-ls/internal/terraform/rootmodule" "github.com/hashicorp/terraform-ls/internal/watcher" "github.com/hashicorp/terraform-ls/langserver/session" ) +type MockSessionInput struct { + RootModules map[string]*rootmodule.RootModuleMock + ManagerTfExecQueue exec.MockItemDispenser +} + type mockSession struct { - mockRMs map[string]*rootmodule.RootModuleMock + mockInput *MockSessionInput stopFunc func() stopFuncCalled bool @@ -23,15 +29,20 @@ func (ms *mockSession) new(srvCtx context.Context) session.Session { sessCtx, stopSession := context.WithCancel(srvCtx) ms.stopFunc = stopSession - logger := testLogger() - rmmm := rootmodule.NewRootModuleManagerMock(ms.mockRMs) + var input *rootmodule.RootModuleManagerMockInput + if ms.mockInput != nil { + input = &rootmodule.RootModuleManagerMockInput{ + RootModules: ms.mockInput.RootModules, + TerraformExecQueue: ms.mockInput.ManagerTfExecQueue, + } + } svc := &service{ - logger: logger, + logger: testLogger(), srvCtx: srvCtx, sessCtx: sessCtx, stopSession: ms.stop, - newRootModuleManager: rmmm, + newRootModuleManager: rootmodule.NewRootModuleManagerMock(input), newWatcher: watcher.MockWatcher(), newWalker: rootmodule.MockWalker, } @@ -56,10 +67,10 @@ func (ms *mockSession) StopFuncCalled() bool { return ms.stopFuncCalled } -func newMockSession(mockRMs map[string]*rootmodule.RootModuleMock) *mockSession { - return &mockSession{mockRMs: mockRMs} +func newMockSession(input *MockSessionInput) *mockSession { + return &mockSession{mockInput: input} } -func NewMockSession(mockRMs map[string]*rootmodule.RootModuleMock) session.SessionFactory { - return newMockSession(mockRMs).new +func NewMockSession(input *MockSessionInput) session.SessionFactory { + return newMockSession(input).new } diff --git a/langserver/handlers/shutdown_test.go b/langserver/handlers/shutdown_test.go index 6a8695b4b..43c6cc004 100644 --- a/langserver/handlers/shutdown_test.go +++ b/langserver/handlers/shutdown_test.go @@ -10,9 +10,10 @@ import ( ) func TestShutdown_twice(t *testing.T) { - ls := langserver.NewLangServerMock(t, NewMockSession(map[string]*rootmodule.RootModuleMock{ - TempDir(t).Dir(): {TerraformExecQueue: validTfMockCalls()}, - })) + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + RootModules: map[string]*rootmodule.RootModuleMock{ + TempDir(t).Dir(): {TerraformExecQueue: validTfMockCalls()}, + }})) stop := ls.Start(t) defer stop()