diff --git a/go.mod b/go.mod index 5a7b2c31..dbc2c307 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/mitchellh/cli v1.1.4 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.5.0 + github.com/otiai10/copy v1.7.0 // indirect github.com/pmezard/go-difflib v1.0.0 github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 diff --git a/go.sum b/go.sum index 17474f3f..aee5597d 100644 --- a/go.sum +++ b/go.sum @@ -462,6 +462,12 @@ github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE= +github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= diff --git a/internal/context/context.go b/internal/context/context.go index d3f149fd..b3e7e31b 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -7,7 +7,6 @@ import ( "github.com/hashicorp/terraform-ls/internal/langserver/diagnostics" lsp "github.com/hashicorp/terraform-ls/internal/protocol" "github.com/hashicorp/terraform-ls/internal/settings" - "github.com/hashicorp/terraform-ls/internal/terraform/module" ) type contextKey struct { @@ -22,7 +21,6 @@ var ( ctxTfExecPath = &contextKey{"terraform executable path"} ctxTfExecLogPath = &contextKey{"terraform executor log path"} ctxTfExecTimeout = &contextKey{"terraform execution timeout"} - ctxWatcher = &contextKey{"watcher"} ctxRootDir = &contextKey{"root directory"} ctxCommandPrefix = &contextKey{"command prefix"} ctxDiagsNotifier = &contextKey{"diagnostics notifier"} @@ -53,18 +51,6 @@ func TerraformExecTimeout(ctx context.Context) (time.Duration, bool) { return path, ok } -func WithWatcher(ctx context.Context, w module.Watcher) context.Context { - return context.WithValue(ctx, ctxWatcher, w) -} - -func Watcher(ctx context.Context) (module.Watcher, error) { - w, ok := ctx.Value(ctxWatcher).(module.Watcher) - if !ok { - return nil, missingContextErr(ctxWatcher) - } - return w, nil -} - func WithTerraformExecPath(ctx context.Context, path string) context.Context { return context.WithValue(ctx, ctxTfExecPath, path) } diff --git a/internal/langserver/handlers/did_change_watched_files.go b/internal/langserver/handlers/did_change_watched_files.go index f9200f1b..b403a2ee 100644 --- a/internal/langserver/handlers/did_change_watched_files.go +++ b/internal/langserver/handlers/did_change_watched_files.go @@ -11,7 +11,9 @@ import ( "github.com/hashicorp/terraform-ls/internal/job" "github.com/hashicorp/terraform-ls/internal/protocol" lsp "github.com/hashicorp/terraform-ls/internal/protocol" + "github.com/hashicorp/terraform-ls/internal/state" "github.com/hashicorp/terraform-ls/internal/terraform/ast" + "github.com/hashicorp/terraform-ls/internal/terraform/datadir" "github.com/hashicorp/terraform-ls/internal/uri" ) @@ -21,6 +23,74 @@ func (svc *service) DidChangeWatchedFiles(ctx context.Context, params lsp.DidCha for _, change := range params.Changes { rawURI := string(change.URI) + // This is necessary because clients may not send delete notifications + // for individual nested files when the parent directory is deleted. + // VS Code / vscode-languageclient behaves this way. + if modUri, ok := datadir.ModuleUriFromDataDir(rawURI); ok { + modHandle := document.DirHandleFromURI(modUri) + if change.Type == protocol.Deleted { + // This is unlikely to happen unless the user manually removed files + // See https://github.com/hashicorp/terraform/issues/30005 + err := svc.modStore.UpdateModManifest(modHandle.Path(), nil, nil) + if err != nil { + svc.logger.Printf("failed to remove module manifest for %q: %s", modHandle, err) + } + } + continue + } + + if modUri, ok := datadir.ModuleUriFromPluginLockFile(rawURI); ok { + if change.Type == protocol.Deleted { + // This is unlikely to happen unless the user manually removed files + // See https://github.com/hashicorp/terraform/issues/30005 + // Cached provider schema could be removed here but it may be useful + // in other modules, so we trade some memory for better UX here. + continue + } + + modHandle := document.DirHandleFromURI(modUri) + err := svc.indexModuleIfNotExists(ctx, modHandle) + if err != nil { + svc.logger.Printf("failed to index module %q: %s", modHandle, err) + continue + } + + jobIds, err := svc.indexer.PluginLockChanged(ctx, modHandle) + if err != nil { + svc.logger.Printf("error refreshing plugins for %q: %s", rawURI, err) + continue + } + ids = append(ids, jobIds...) + continue + } + + if modUri, ok := datadir.ModuleUriFromModuleLockFile(rawURI); ok { + modHandle := document.DirHandleFromURI(modUri) + if change.Type == protocol.Deleted { + // This is unlikely to happen unless the user manually removed files + // See https://github.com/hashicorp/terraform/issues/30005 + err := svc.modStore.UpdateModManifest(modHandle.Path(), nil, nil) + if err != nil { + svc.logger.Printf("failed to remove module manifest for %q: %s", modHandle, err) + } + continue + } + + err := svc.indexModuleIfNotExists(ctx, modHandle) + if err != nil { + svc.logger.Printf("failed to index module %q: %s", modHandle, err) + continue + } + + jobIds, err := svc.indexer.ModuleManifestChanged(ctx, modHandle) + if err != nil { + svc.logger.Printf("error refreshing plugins for %q: %s", modHandle, err) + continue + } + ids = append(ids, jobIds...) + continue + } + rawPath, err := uri.PathFromURI(rawURI) if err != nil { svc.logger.Printf("error parsing %q: %s", rawURI, err) @@ -137,6 +207,25 @@ func (svc *service) DidChangeWatchedFiles(ctx context.Context, params lsp.DidCha return nil } +func (svc *service) indexModuleIfNotExists(ctx context.Context, modHandle document.DirHandle) error { + _, err := svc.modStore.ModuleByPath(modHandle.Path()) + if err != nil { + if state.IsModuleNotFound(err) { + err = svc.stateStore.WalkerPaths.EnqueueDir(modHandle) + if err != nil { + return fmt.Errorf("failed to walk module %q: %w", modHandle, err) + } + err = svc.stateStore.WalkerPaths.WaitForDirs(ctx, []document.DirHandle{modHandle}) + if err != nil { + return fmt.Errorf("failed to wait for module walk %q: %w", modHandle, err) + } + } else { + return fmt.Errorf("failed to find module %q: %w", modHandle, err) + } + } + return nil +} + func modHandleFromRawOsPath(ctx context.Context, rawPath string) (*parsedModuleHandle, error) { fi, err := os.Stat(rawPath) if err != nil { diff --git a/internal/langserver/handlers/did_change_watched_files_test.go b/internal/langserver/handlers/did_change_watched_files_test.go index bed61dad..288c8070 100644 --- a/internal/langserver/handlers/did_change_watched_files_test.go +++ b/internal/langserver/handlers/did_change_watched_files_test.go @@ -1,6 +1,7 @@ package handlers import ( + "context" "fmt" "os" "path/filepath" @@ -8,12 +9,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-version" + install "github.com/hashicorp/hc-install" + "github.com/hashicorp/hc-install/product" + "github.com/hashicorp/hc-install/releases" + "github.com/hashicorp/hc-install/src" tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/langserver" "github.com/hashicorp/terraform-ls/internal/state" "github.com/hashicorp/terraform-ls/internal/terraform/exec" "github.com/hashicorp/terraform-ls/internal/terraform/module" + tfaddr "github.com/hashicorp/terraform-registry-address" + "github.com/otiai10/copy" "github.com/stretchr/testify/mock" ) @@ -687,3 +694,235 @@ func TestLangServer_DidChangeWatchedFiles_delete_dir(t *testing.T) { t.Fatalf("expected module at %q to be gone", tmpDir.Path()) } } + +func TestLangServer_DidChangeWatchedFiles_pluginChange(t *testing.T) { + testData, err := filepath.Abs("testdata") + if err != nil { + t.Fatal(err) + } + + originalTestDir := filepath.Join(testData, "uninitialized-single-submodule") + testDir := t.TempDir() + // Copy test configuration so the test can run in isolation + err = copy.Copy(originalTestDir, testDir) + if err != nil { + t.Fatal(err) + } + + testHandle := document.DirHandleFromPath(testDir) + + ss, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + wc := module.NewWalkerCollector() + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + TerraformCalls: &exec.TerraformMockCalls{ + PerWorkDir: map[string][]*mock.Call{ + testHandle.Path(): { + { + Method: "Version", + Repeatability: 2, + Arguments: []interface{}{ + mock.AnythingOfType(""), + }, + ReturnArguments: []interface{}{ + version.Must(version.NewVersion("0.12.0")), + nil, + nil, + }, + }, + { + Method: "GetExecPath", + Repeatability: 2, + ReturnArguments: []interface{}{ + "", + }, + }, + { + Method: "ProviderSchemas", + Repeatability: 2, + Arguments: []interface{}{ + mock.AnythingOfType(""), + }, + ReturnArguments: []interface{}{ + &tfjson.ProviderSchemas{ + FormatVersion: "0.1", + Schemas: map[string]*tfjson.ProviderSchema{ + "test": { + ConfigSchema: &tfjson.Schema{}, + }, + }, + }, + nil, + }, + }, + }, + }, + }, + StateStore: ss, + WalkerCollector: wc, + })) + stop := ls.Start(t) + defer stop() + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, testHandle.URI)}) + waitForWalkerPath(t, ss, wc, testHandle) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + + addr := tfaddr.MustParseRawProviderSourceString("-/test") + vc := version.MustConstraints(version.NewConstraint(">= 1.0")) + + _, err = ss.ProviderSchemas.ProviderSchema(testHandle.Path(), addr, vc) + if err == nil { + t.Fatal("expected -/test schema to be missing") + } + + // Install Terraform + tfVersion := version.Must(version.NewVersion("1.1.7")) + i := install.NewInstaller() + ctx := context.Background() + execPath, err := i.Install(ctx, []src.Installable{ + &releases.ExactVersion{ + Product: product.Terraform, + Version: tfVersion, + }, + }) + if err != nil { + t.Fatal(err) + } + + // Install submodule + tf, err := exec.NewExecutor(testHandle.Path(), execPath) + if err != nil { + t.Fatal(err) + } + err = tf.Init(ctx) + if err != nil { + t.Fatal(err) + } + + ls.Call(t, &langserver.CallRequest{ + Method: "workspace/didChangeWatchedFiles", + ReqParams: fmt.Sprintf(`{ + "changes": [ + { + "uri": "%s/.terraform.lock.hcl", + "type": 1 + } + ] +}`, testHandle.URI)}) + + _, err = ss.ProviderSchemas.ProviderSchema(testHandle.Path(), addr, vc) + if err != nil { + t.Fatal(err) + } +} + +func TestLangServer_DidChangeWatchedFiles_moduleInstalled(t *testing.T) { + testData, err := filepath.Abs("testdata") + if err != nil { + t.Fatal(err) + } + + originalTestDir := filepath.Join(testData, "uninitialized-single-submodule") + testDir := t.TempDir() + // Copy test configuration so the test can run in isolation + err = copy.Copy(originalTestDir, testDir) + if err != nil { + t.Fatal(err) + } + + testHandle := document.DirHandleFromPath(testDir) + + ss, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + wc := module.NewWalkerCollector() + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + TerraformCalls: &exec.TerraformMockCalls{ + PerWorkDir: map[string][]*mock.Call{ + testHandle.Path(): validTfMockCalls(), + }, + }, + StateStore: ss, + WalkerCollector: wc, + })) + stop := ls.Start(t) + defer stop() + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, testHandle.URI)}) + waitForWalkerPath(t, ss, wc, testHandle) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + + submodulePath := filepath.Join(testDir, "application") + _, err = ss.Modules.ModuleByPath(submodulePath) + if err == nil || !state.IsModuleNotFound(err) { + t.Fatalf("expected submodule not to be found: %s", err) + } + + // Install Terraform + tfVersion := version.Must(version.NewVersion("1.1.7")) + i := install.NewInstaller() + ctx := context.Background() + execPath, err := i.Install(ctx, []src.Installable{ + &releases.ExactVersion{ + Product: product.Terraform, + Version: tfVersion, + }, + }) + if err != nil { + t.Fatal(err) + } + + // Install submodule + tf, err := exec.NewExecutor(testHandle.Path(), execPath) + if err != nil { + t.Fatal(err) + } + err = tf.Get(ctx) + if err != nil { + t.Fatal(err) + } + + ls.Call(t, &langserver.CallRequest{ + Method: "workspace/didChangeWatchedFiles", + ReqParams: fmt.Sprintf(`{ + "changes": [ + { + "uri": "%s/.terraform/modules/modules.json", + "type": 1 + } + ] +}`, testHandle.URI)}) + + mod, err := ss.Modules.ModuleByPath(submodulePath) + if err != nil { + t.Fatal(err) + } + + if len(mod.Meta.Variables) != 3 { + t.Fatalf("expected exactly 3 variables, %d given", len(mod.Meta.Variables)) + } +} diff --git a/internal/langserver/handlers/did_change_workspace_folders.go b/internal/langserver/handlers/did_change_workspace_folders.go index 77518f11..4a07bd53 100644 --- a/internal/langserver/handlers/did_change_workspace_folders.go +++ b/internal/langserver/handlers/did_change_workspace_folders.go @@ -33,12 +33,6 @@ func (svc *service) indexNewModule(ctx context.Context, modURI string) { }) return } - - err = svc.watcher.AddModule(modHandle.Path()) - if err != nil { - svc.logger.Printf("failed to add module to watcher: %s", err) - return - } } func (svc *service) removeIndexedModule(ctx context.Context, modURI string) { @@ -54,12 +48,6 @@ func (svc *service) removeIndexedModule(ctx context.Context, modURI string) { return } - err = svc.watcher.RemoveModule(modHandle.Path()) - if err != nil { - svc.logger.Printf("failed to remove module from watcher: %s", err) - return - } - err = svc.stateStore.JobStore.DequeueJobsForDir(modHandle) if err != nil { svc.logger.Printf("failed to dequeue jobs for module: %s", err) diff --git a/internal/langserver/handlers/did_open.go b/internal/langserver/handlers/did_open.go index 2e544c3d..0d41f52c 100644 --- a/internal/langserver/handlers/did_open.go +++ b/internal/langserver/handlers/did_open.go @@ -3,7 +3,6 @@ package handlers import ( "context" - lsctx "github.com/hashicorp/terraform-ls/internal/context" "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/job" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" @@ -65,11 +64,6 @@ func (svc *service) TextDocumentDidOpen(ctx context.Context, params lsp.DidOpenT jobIds = append(jobIds, jobId) } - watcher, err := lsctx.Watcher(ctx) - if err != nil { - return err - } - if svc.singleFileMode { err = svc.stateStore.WalkerPaths.EnqueueDir(modHandle) if err != nil { @@ -77,13 +71,6 @@ func (svc *service) TextDocumentDidOpen(ctx context.Context, params lsp.DidOpenT } } - if !watcher.IsModuleWatched(mod.Path) { - err := watcher.AddModule(mod.Path) - if err != nil { - return err - } - } - return svc.stateStore.JobStore.WaitForJobs(ctx, jobIds...) } diff --git a/internal/langserver/handlers/initialize.go b/internal/langserver/handlers/initialize.go index 04eb9065..fad5a443 100644 --- a/internal/langserver/handlers/initialize.go +++ b/internal/langserver/handlers/initialize.go @@ -281,27 +281,6 @@ func (svc *service) setupWalker(ctx context.Context, params lsp.InitializeParams svc.openDirWalker.SetIgnoreDirectoryNames(options.IgnoreDirectoryNames) svc.openDirWalker.SetExcludeModulePaths(excludeModulePaths) - if len(options.ModulePaths) > 0 { - svc.logger.Printf("Attempting to add %d static module paths", len(options.ModulePaths)) - for _, rawPath := range options.ModulePaths { - modPath, err := resolvePath(root.Path(), rawPath) - if err != nil { - jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ - Type: lsp.Warning, - Message: fmt.Sprintf("Ignoring module path %s: %s", rawPath, err), - }) - continue - } - - err = svc.watcher.AddModule(modPath) - if err != nil { - return err - } - } - - return nil - } - return nil } diff --git a/internal/langserver/handlers/initialize_benchmarks_test.go b/internal/langserver/handlers/initialize_benchmarks_test.go index 4ff5c318..80d752ba 100644 --- a/internal/langserver/handlers/initialize_benchmarks_test.go +++ b/internal/langserver/handlers/initialize_benchmarks_test.go @@ -129,7 +129,6 @@ func BenchmarkInitializeFolder_basic(b *testing.B) { srvCtx: ctx, sessCtx: sessCtx, stopSession: stopSession, - newWatcher: module.MockWatcher(), tfDiscoFunc: d.LookPath, tfExecFactory: exec.NewExecutor, walkerCollector: wc, diff --git a/internal/langserver/handlers/initialized.go b/internal/langserver/handlers/initialized.go index 2307e354..3061d6ae 100644 --- a/internal/langserver/handlers/initialized.go +++ b/internal/langserver/handlers/initialized.go @@ -3,9 +3,69 @@ package handlers import ( "context" + "github.com/creachadair/jrpc2" + "github.com/hashicorp/go-uuid" + ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" + "github.com/hashicorp/terraform-ls/internal/terraform/datadir" ) -func Initialized(ctx context.Context, params lsp.InitializedParams) error { +func (svc *service) Initialized(ctx context.Context, params lsp.InitializedParams) error { + caps, err := ilsp.ClientCapabilities(ctx) + if err != nil { + return err + } + + return svc.setupWatchedFiles(ctx, caps.Workspace.DidChangeWatchedFiles) +} + +func (svc *service) setupWatchedFiles(ctx context.Context, caps lsp.DidChangeWatchedFilesClientCapabilities) error { + if !caps.DynamicRegistration { + svc.logger.Printf("Client doesn't support dynamic watched files registration, " + + "provider and module changes may not be reflected at runtime") + return nil + } + + id, err := uuid.GenerateUUID() + if err != nil { + return err + } + + watchPatterns := datadir.PathGlobPatternsForWatching() + watchers := make([]lsp.FileSystemWatcher, len(watchPatterns)) + for i, wp := range watchPatterns { + watchers[i] = lsp.FileSystemWatcher{ + GlobPattern: wp.Pattern, + Kind: kindFromEventType(wp.EventType), + } + } + + srv := jrpc2.ServerFromContext(ctx) + _, err = srv.Callback(ctx, "client/registerCapability", lsp.RegistrationParams{ + Registrations: []lsp.Registration{ + { + ID: id, + Method: "workspace/didChangeWatchedFiles", + RegisterOptions: lsp.DidChangeWatchedFilesRegistrationOptions{ + Watchers: watchers, + }, + }, + }, + }) + if err != nil { + svc.logger.Printf("failed to register watched files: %s", err) + } return nil } + +func kindFromEventType(eventType datadir.EventType) uint32 { + switch eventType { + case datadir.CreateEventType: + return uint32(lsp.Created) + case datadir.ModifyEventType: + return uint32(lsp.Changed) + case datadir.DeleteEventType: + return uint32(lsp.Deleted) + } + return 0 +} diff --git a/internal/langserver/handlers/service.go b/internal/langserver/handlers/service.go index cd9f3d6d..2c7fbfd6 100644 --- a/internal/langserver/handlers/service.go +++ b/internal/langserver/handlers/service.go @@ -49,8 +49,6 @@ type service struct { fs *filesystem.Filesystem modStore *state.ModuleStore schemaStore *state.ProviderSchemaStore - watcher module.Watcher - newWatcher module.WatcherFactory tfDiscoFunc discovery.DiscoveryFunc tfExecFactory exec.ExecutorFactory tfExecOpts *exec.ExecutorOpts @@ -60,6 +58,7 @@ type service struct { server session.Server diagsNotifier *diagnostics.Notifier notifier *notifier.Notifier + indexer *module.Indexer walkerCollector *module.WalkerCollector additionalHandlers map[string]rpch.Func @@ -78,7 +77,6 @@ func NewSession(srvCtx context.Context) session.Session { srvCtx: srvCtx, sessCtx: sessCtx, stopSession: stopSession, - newWatcher: module.NewWatcher, tfDiscoFunc: d.LookPath, tfExecFactory: exec.NewExecutor, telemetry: &telemetry.NoopSender{}, @@ -136,7 +134,9 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return nil, err } - return handle(ctx, req, Initialized) + ctx = ilsp.WithClientCapabilities(ctx, cc) + + return handle(ctx, req, svc.Initialized) }, "textDocument/didChange": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { err := session.CheckInitializationIsConfirmed() @@ -150,7 +150,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { if err != nil { return nil, err } - ctx = lsctx.WithWatcher(ctx, svc.watcher) return handle(ctx, req, svc.TextDocumentDidOpen) }, "textDocument/didClose": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { @@ -285,8 +284,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return nil, err } - ctx = lsctx.WithWatcher(ctx, svc.watcher) - return handle(ctx, req, svc.DidChangeWorkspaceFolders) }, "workspace/didChangeWatchedFiles": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { @@ -312,7 +309,6 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { } ctx = lsctx.WithCommandPrefix(ctx, &commandPrefix) - ctx = lsctx.WithWatcher(ctx, svc.watcher) ctx = lsctx.WithRootDirectory(ctx, &rootDir) ctx = lsctx.WithDiagnosticsNotifier(ctx, svc.diagsNotifier) ctx = ilsp.ContextWithClientName(ctx, &clientName) @@ -476,6 +472,8 @@ func (svc *service) configureSessionDependencies(ctx context.Context, cfgOpts *s svc.fs = filesystem.NewFilesystem(svc.stateStore.DocumentStore) svc.fs.SetLogger(svc.logger) + svc.indexer = module.NewIndexer(svc.fs, svc.modStore, svc.schemaStore, svc.stateStore.JobStore, svc.tfExecFactory) + svc.decoder = idecoder.NewDecoder(ctx, &idecoder.PathReader{ ModuleReader: svc.modStore, SchemaReader: svc.schemaStore, @@ -496,17 +494,6 @@ func (svc *service) configureSessionDependencies(ctx context.Context, cfgOpts *s svc.closedDirWalker.Collector = svc.walkerCollector svc.openDirWalker.SetLogger(svc.logger) - ww, err := svc.newWatcher(svc.fs, svc.modStore, svc.stateStore.ProviderSchemas, svc.stateStore.JobStore, svc.tfExecFactory) - if err != nil { - return err - } - svc.watcher = ww - svc.watcher.SetLogger(svc.logger) - err = svc.watcher.Start(ctx) - if err != nil { - return err - } - return nil } @@ -541,16 +528,6 @@ func (svc *service) shutdown() { svc.logger.Printf("openDirWalker stopped") } - if svc.watcher != nil { - svc.logger.Println("stopping watcher for session ...") - err := svc.watcher.Stop() - if err != nil { - svc.logger.Println("unable to stop watcher for session:", err) - } else { - svc.logger.Println("watcher stopped") - } - } - if svc.closedDirIndexer != nil { svc.closedDirIndexer.Stop() } diff --git a/internal/langserver/handlers/session_mock_test.go b/internal/langserver/handlers/session_mock_test.go index 523e33b4..0a7dd2cd 100644 --- a/internal/langserver/handlers/session_mock_test.go +++ b/internal/langserver/handlers/session_mock_test.go @@ -58,7 +58,6 @@ func (ms *mockSession) new(srvCtx context.Context) session.Session { srvCtx: srvCtx, sessCtx: sessCtx, stopSession: ms.stop, - newWatcher: module.MockWatcher(), tfDiscoFunc: d.LookPath, tfExecFactory: exec.NewMockExecutor(tfCalls), additionalHandlers: handlers, diff --git a/internal/langserver/handlers/testdata/uninitialized-single-submodule/.gitignore b/internal/langserver/handlers/testdata/uninitialized-single-submodule/.gitignore new file mode 100644 index 00000000..1c99dc13 --- /dev/null +++ b/internal/langserver/handlers/testdata/uninitialized-single-submodule/.gitignore @@ -0,0 +1 @@ +.terraform/ diff --git a/internal/langserver/handlers/testdata/uninitialized-single-submodule/application/main.tf b/internal/langserver/handlers/testdata/uninitialized-single-submodule/application/main.tf new file mode 100644 index 00000000..757a72ea --- /dev/null +++ b/internal/langserver/handlers/testdata/uninitialized-single-submodule/application/main.tf @@ -0,0 +1,18 @@ +variable "environment_name" { + type = string +} + +variable "app_prefix" { + type = string +} + +variable "instances" { + type = number +} + +resource "random_pet" "application" { + count = var.instances + keepers = { + unique = "${var.environment_name}-${var.app_prefix}" + } +} diff --git a/internal/langserver/handlers/testdata/uninitialized-single-submodule/main.tf b/internal/langserver/handlers/testdata/uninitialized-single-submodule/main.tf new file mode 100644 index 00000000..6a0bc50e --- /dev/null +++ b/internal/langserver/handlers/testdata/uninitialized-single-submodule/main.tf @@ -0,0 +1,6 @@ +module "gorilla-app" { + source = "./application" + environment_name = "prod" + app_prefix = "protect-gorillas" + instances = 5 +} diff --git a/internal/terraform/datadir/paths.go b/internal/terraform/datadir/paths.go index 152ef6b7..d180954d 100644 --- a/internal/terraform/datadir/paths.go +++ b/internal/terraform/datadir/paths.go @@ -1,8 +1,10 @@ package datadir import ( + "path" "path/filepath" "runtime" + "strings" ) const DataDirName = ".terraform" @@ -28,3 +30,68 @@ func watchableModuleDirs(modPath string) []string { filepath.Join(modPath, DataDirName, "plugins", runtime.GOOS+"_"+runtime.GOARCH), } } + +type EventType rune + +const ( + AnyEventType EventType = '*' + CreateEventType EventType = 'c' + ModifyEventType EventType = 'm' + DeleteEventType EventType = 'd' +) + +type WatchPattern struct { + Pattern string + EventType EventType +} + +func PathGlobPatternsForWatching() []WatchPattern { + patterns := make([]WatchPattern, 0) + + // This is necessary because clients may not send delete notifications + // for individual nested files when the parent directory is deleted. + // VS Code / vscode-languageclient behaves this way. + patterns = append(patterns, WatchPattern{ + Pattern: "**/" + DataDirName, + EventType: DeleteEventType, + }) + + patterns = append(patterns, WatchPattern{ + Pattern: "**/" + path.Join(manifestPathElements...), + EventType: AnyEventType, + }) + for _, pElems := range pluginLockFilePathElements { + patterns = append(patterns, WatchPattern{ + Pattern: "**/" + path.Join(pElems...), + EventType: AnyEventType, + }) + } + + return patterns +} + +func ModuleUriFromDataDir(rawUri string) (string, bool) { + suffix := "/" + DataDirName + if strings.HasSuffix(rawUri, suffix) { + return strings.TrimSuffix(rawUri, suffix), true + } + return "", false +} + +func ModuleUriFromPluginLockFile(rawUri string) (string, bool) { + for _, pathElems := range pluginLockFilePathElements { + suffix := "/" + path.Join(pathElems...) + if strings.HasSuffix(rawUri, suffix) { + return strings.TrimSuffix(rawUri, suffix), true + } + } + return "", false +} + +func ModuleUriFromModuleLockFile(rawUri string) (string, bool) { + suffix := "/" + path.Join(manifestPathElements...) + if strings.HasSuffix(rawUri, suffix) { + return strings.TrimSuffix(rawUri, suffix), true + } + return "", false +} diff --git a/internal/terraform/module/indexer.go b/internal/terraform/module/indexer.go new file mode 100644 index 00000000..9b027153 --- /dev/null +++ b/internal/terraform/module/indexer.go @@ -0,0 +1,213 @@ +package module + +import ( + "context" + "os" + + "github.com/hashicorp/terraform-ls/internal/document" + "github.com/hashicorp/terraform-ls/internal/job" + "github.com/hashicorp/terraform-ls/internal/state" + "github.com/hashicorp/terraform-ls/internal/terraform/exec" + op "github.com/hashicorp/terraform-ls/internal/terraform/module/operation" +) + +type Indexer struct { + fs ReadOnlyFS + modStore *state.ModuleStore + schemaStore *state.ProviderSchemaStore + jobStore job.JobStore + tfExecFactory exec.ExecutorFactory +} + +func NewIndexer(fs ReadOnlyFS, modStore *state.ModuleStore, schemaStore *state.ProviderSchemaStore, + jobStore job.JobStore, tfExec exec.ExecutorFactory) *Indexer { + return &Indexer{ + fs: fs, + modStore: modStore, + schemaStore: schemaStore, + jobStore: jobStore, + tfExecFactory: tfExec, + } +} + +func (idx *Indexer) ModuleManifestChanged(ctx context.Context, modHandle document.DirHandle) (job.IDs, error) { + ids := make(job.IDs, 0) + + id, err := idx.jobStore.EnqueueJob(job.Job{ + Dir: modHandle, + Func: func(ctx context.Context) error { + return ParseModuleManifest(idx.fs, idx.modStore, modHandle.Path()) + }, + Type: op.OpTypeParseModuleManifest.String(), + Defer: decodeInstalledModuleCalls(idx.fs, idx.modStore, idx.schemaStore, modHandle.Path()), + }) + if err != nil { + return ids, err + } + ids = append(ids, id) + + return ids, nil +} + +func (idx *Indexer) PluginLockChanged(ctx context.Context, modHandle document.DirHandle) (job.IDs, error) { + ids := make(job.IDs, 0) + + id, err := idx.jobStore.EnqueueJob(job.Job{ + Dir: modHandle, + Func: func(ctx context.Context) error { + ctx = exec.WithExecutorFactory(ctx, idx.tfExecFactory) + eo, ok := exec.ExecutorOptsFromContext(ctx) + if ok { + ctx = exec.WithExecutorOpts(ctx, eo) + } + + return ObtainSchema(ctx, idx.modStore, idx.schemaStore, modHandle.Path()) + }, + Type: op.OpTypeObtainSchema.String(), + }) + if err != nil { + return ids, err + } + ids = append(ids, id) + + id, err = idx.jobStore.EnqueueJob(job.Job{ + Dir: modHandle, + Func: func(ctx context.Context) error { + ctx = exec.WithExecutorFactory(ctx, idx.tfExecFactory) + eo, ok := exec.ExecutorOptsFromContext(ctx) + if ok { + ctx = exec.WithExecutorOpts(ctx, eo) + } + + return GetTerraformVersion(ctx, idx.modStore, modHandle.Path()) + }, + Type: op.OpTypeGetTerraformVersion.String(), + }) + if err != nil { + return ids, err + } + ids = append(ids, id) + + return ids, nil +} + +func decodeInstalledModuleCalls(fs ReadOnlyFS, modStore *state.ModuleStore, schemaReader state.SchemaReader, modPath string) job.DeferFunc { + return func(ctx context.Context, opErr error) (jobIds job.IDs) { + if opErr != nil { + return + } + + moduleCalls, err := modStore.ModuleCalls(modPath) + if err != nil { + return + } + + jobStore, err := job.JobStoreFromContext(ctx) + if err != nil { + return + } + + for _, mc := range moduleCalls.Installed { + fi, err := os.Stat(mc.Path) + if err != nil || !fi.IsDir() { + continue + } + modStore.Add(mc.Path) + + mcHandle := document.DirHandleFromPath(mc.Path) + // copy path for queued jobs below + mcPath := mc.Path + + id, err := jobStore.EnqueueJob(job.Job{ + Dir: mcHandle, + Func: func(ctx context.Context) error { + return ParseModuleConfiguration(fs, modStore, mcPath) + }, + Type: op.OpTypeParseModuleConfiguration.String(), + Defer: func(ctx context.Context, jobErr error) (ids job.IDs) { + id, err := jobStore.EnqueueJob(job.Job{ + Dir: mcHandle, + Type: op.OpTypeLoadModuleMetadata.String(), + Func: func(ctx context.Context) error { + return LoadModuleMetadata(modStore, mcPath) + }, + }) + if err != nil { + return + } + ids = append(ids, id) + + rIds := collectReferences(ctx, mcHandle, modStore, schemaReader) + ids = append(ids, rIds...) + + return + }, + }) + if err != nil { + return + } + jobIds = append(jobIds, id) + + id, err = jobStore.EnqueueJob(job.Job{ + Dir: mcHandle, + Func: func(ctx context.Context) error { + return ParseVariables(fs, modStore, mcPath) + }, + Type: op.OpTypeParseVariables.String(), + Defer: func(ctx context.Context, jobErr error) (ids job.IDs) { + id, err = jobStore.EnqueueJob(job.Job{ + Dir: mcHandle, + Func: func(ctx context.Context) error { + return DecodeVarsReferences(ctx, modStore, schemaReader, mcPath) + }, + Type: op.OpTypeDecodeVarsReferences.String(), + }) + if err != nil { + return + } + ids = append(ids, id) + return + }, + }) + if err != nil { + return + } + jobIds = append(jobIds, id) + } + + return + } +} + +func collectReferences(ctx context.Context, dirHandle document.DirHandle, modStore *state.ModuleStore, schemaReader state.SchemaReader) (ids job.IDs) { + jobStore, err := job.JobStoreFromContext(ctx) + if err != nil { + return + } + + id, err := jobStore.EnqueueJob(job.Job{ + Dir: dirHandle, + Func: func(ctx context.Context) error { + return DecodeReferenceTargets(ctx, modStore, schemaReader, dirHandle.Path()) + }, + Type: op.OpTypeDecodeReferenceTargets.String(), + }) + if err != nil { + return + } + ids = append(ids, id) + + id, err = jobStore.EnqueueJob(job.Job{ + Dir: dirHandle, + Func: func(ctx context.Context) error { + return DecodeReferenceOrigins(ctx, modStore, schemaReader, dirHandle.Path()) + }, + Type: op.OpTypeDecodeReferenceOrigins.String(), + }) + if err != nil { + return + } + ids = append(ids, id) + + return +} diff --git a/internal/terraform/module/module_manager_test.go b/internal/terraform/module/module_manager_test.go index 0910f961..5c0e01f0 100644 --- a/internal/terraform/module/module_manager_test.go +++ b/internal/terraform/module/module_manager_test.go @@ -14,6 +14,7 @@ import ( tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/filesystem" + "github.com/hashicorp/terraform-ls/internal/job" "github.com/hashicorp/terraform-ls/internal/scheduler" "github.com/hashicorp/terraform-ls/internal/state" "github.com/hashicorp/terraform-ls/internal/terraform/exec" @@ -351,3 +352,23 @@ func testLogger() *log.Logger { return log.New(ioutil.Discard, "", 0) } + +type closedJobStore struct { + js *state.JobStore +} + +func (js *closedJobStore) EnqueueJob(newJob job.Job) (job.ID, error) { + return js.js.EnqueueJob(newJob) +} + +func (js *closedJobStore) AwaitNextJob(ctx context.Context) (job.ID, job.Job, error) { + return js.js.AwaitNextJob(ctx, false) +} + +func (js *closedJobStore) FinishJob(id job.ID, jobErr error, deferredJobIds ...job.ID) error { + return js.js.FinishJob(id, jobErr, deferredJobIds...) +} + +func (js *closedJobStore) WaitForJobs(ctx context.Context, jobIds ...job.ID) error { + return js.js.WaitForJobs(ctx, jobIds...) +} diff --git a/internal/terraform/module/walker.go b/internal/terraform/module/walker.go index 2bb16ae9..865972c2 100644 --- a/internal/terraform/module/walker.go +++ b/internal/terraform/module/walker.go @@ -291,7 +291,7 @@ func (w *Walker) walk(ctx context.Context, dir document.DirHandle) error { return ParseModuleManifest(w.fs, w.modStore, dir) }, Type: op.OpTypeParseModuleManifest.String(), - Defer: decodeCalledModulesFunc(w.fs, w.modStore, w.schemaStore, w.watcher, dir), + Defer: decodeInstalledModuleCalls(w.fs, w.modStore, w.schemaStore, dir), }) if err != nil { return err diff --git a/internal/terraform/module/watcher.go b/internal/terraform/module/watcher.go deleted file mode 100644 index 09141c21..00000000 --- a/internal/terraform/module/watcher.go +++ /dev/null @@ -1,517 +0,0 @@ -package module - -import ( - "context" - "io/ioutil" - "log" - "os" - "path/filepath" - - "github.com/fsnotify/fsnotify" - "github.com/hashicorp/terraform-ls/internal/document" - "github.com/hashicorp/terraform-ls/internal/job" - "github.com/hashicorp/terraform-ls/internal/pathcmp" - "github.com/hashicorp/terraform-ls/internal/state" - "github.com/hashicorp/terraform-ls/internal/terraform/datadir" - "github.com/hashicorp/terraform-ls/internal/terraform/exec" - op "github.com/hashicorp/terraform-ls/internal/terraform/module/operation" -) - -// Watcher is a wrapper around native fsnotify.Watcher -// It provides the ability to detect actual file changes -// (rather than just events that may not be changing any bytes) -type watcher struct { - fw *fsnotify.Watcher - - fs ReadOnlyFS - modStore *state.ModuleStore - schemaStore *state.ProviderSchemaStore - jobStore job.JobStore - tfExecFactory exec.ExecutorFactory - - modules []*watchedModule - logger *log.Logger - - watching bool - cancelFunc context.CancelFunc -} - -type WatcherFactory func(fs ReadOnlyFS, ms *state.ModuleStore, pss *state.ProviderSchemaStore, js job.JobStore, tfExec exec.ExecutorFactory) (Watcher, error) - -type watchedModule struct { - Path string - Watched []string - Watchable *datadir.WatchablePaths -} - -func NewWatcher(fs ReadOnlyFS, ms *state.ModuleStore, pss *state.ProviderSchemaStore, js job.JobStore, tfExec exec.ExecutorFactory) (Watcher, error) { - fw, err := fsnotify.NewWatcher() - if err != nil { - return nil, err - } - - return &watcher{ - fw: fw, - fs: fs, - modStore: ms, - schemaStore: pss, - jobStore: js, - tfExecFactory: tfExec, - logger: defaultLogger, - modules: make([]*watchedModule, 0), - }, nil -} - -var defaultLogger = log.New(ioutil.Discard, "", 0) - -func (w *watcher) SetLogger(logger *log.Logger) { - w.logger = logger -} - -func (w *watcher) IsModuleWatched(modPath string) bool { - modPath = filepath.Clean(modPath) - - for _, m := range w.modules { - if pathcmp.PathEquals(m.Path, modPath) { - return true - } - } - - return false -} - -func (w *watcher) AddModule(modPath string) error { - modPath = filepath.Clean(modPath) - - w.logger.Printf("adding module for watching: %s", modPath) - - wm := &watchedModule{ - Path: modPath, - Watched: make([]string, 0), - Watchable: datadir.WatchableModulePaths(modPath), - } - w.modules = append(w.modules, wm) - - // We watch individual dirs (instead of individual files). - // This does result in more events but fewer watched paths. - // fsnotify does not support recursive watching yet. - // See https://github.com/fsnotify/fsnotify/issues/18 - - err := w.fw.Add(modPath) - if err != nil { - return err - } - - for _, dirPath := range wm.Watchable.Dirs { - err := w.fw.Add(dirPath) - if err == nil { - wm.Watched = append(wm.Watched, dirPath) - } - } - - return nil -} - -func (w *watcher) RemoveModule(modPath string) error { - modPath = filepath.Clean(modPath) - - w.logger.Printf("removing module from watching: %s", modPath) - - for modI, mod := range w.modules { - if pathcmp.PathEquals(mod.Path, modPath) { - for _, wPath := range mod.Watched { - w.fw.Remove(wPath) - } - w.fw.Remove(mod.Path) - w.modules = append(w.modules[:modI], w.modules[modI+1:]...) - } - - for i, wp := range mod.Watched { - if pathcmp.PathEquals(wp, modPath) { - w.fw.Remove(wp) - mod.Watched = append(mod.Watched[:i], mod.Watched[i+1:]...) - } - } - } - - return nil -} - -func (w *watcher) run(ctx context.Context) { - for { - select { - case event, ok := <-w.fw.Events: - if !ok { - return - } - w.processEvent(ctx, event) - case err, ok := <-w.fw.Errors: - if !ok { - return - } - w.logger.Println("watch error:", err) - } - } -} - -func (w *watcher) processEvent(ctx context.Context, event fsnotify.Event) { - eventPath := event.Name - - if event.Op&fsnotify.Write == fsnotify.Write { - for _, mod := range w.modules { - modHandle := document.DirHandleFromPath(mod.Path) - if containsPath(mod.Watchable.ModuleManifests, eventPath) { - id, err := w.jobStore.EnqueueJob(job.Job{ - Dir: modHandle, - Func: func(ctx context.Context) error { - return ParseModuleManifest(w.fs, w.modStore, mod.Path) - }, - Type: op.OpTypeParseModuleManifest.String(), - Defer: decodeCalledModulesFunc(w.fs, w.modStore, w.schemaStore, w, mod.Path), - }) - if err == nil { - w.jobStore.WaitForJobs(ctx, id) - collectReferences(ctx, modHandle, w.modStore, w.schemaStore) - } - - return - } - if containsPath(mod.Watchable.PluginLockFiles, eventPath) { - w.jobStore.EnqueueJob(job.Job{ - Dir: modHandle, - Func: func(ctx context.Context) error { - ctx = exec.WithExecutorFactory(ctx, w.tfExecFactory) - eo, ok := exec.ExecutorOptsFromContext(ctx) - if ok { - ctx = exec.WithExecutorOpts(ctx, eo) - } - - return ObtainSchema(ctx, w.modStore, w.schemaStore, mod.Path) - }, - Type: op.OpTypeObtainSchema.String(), - }) - w.jobStore.EnqueueJob(job.Job{ - Dir: modHandle, - Func: func(ctx context.Context) error { - ctx = exec.WithExecutorFactory(ctx, w.tfExecFactory) - eo, ok := exec.ExecutorOptsFromContext(ctx) - if ok { - ctx = exec.WithExecutorOpts(ctx, eo) - } - - return GetTerraformVersion(ctx, w.modStore, mod.Path) - }, - Type: op.OpTypeGetTerraformVersion.String(), - }) - return - } - } - } - - if event.Op&fsnotify.Create == fsnotify.Create { - for _, mod := range w.modules { - modHandle := document.DirHandleFromPath(mod.Path) - - if containsPath(mod.Watchable.Dirs, eventPath) { - w.fw.Add(eventPath) - mod.Watched = append(mod.Watched, eventPath) - - filepath.Walk(eventPath, func(path string, info os.FileInfo, err error) error { - if info.IsDir() { - if containsPath(mod.Watchable.Dirs, path) { - w.fw.Add(path) - mod.Watched = append(mod.Watched, path) - } - return nil - } - - modHandle := document.DirHandleFromPath(path) - - if containsPath(mod.Watchable.ModuleManifests, path) { - id, err := w.jobStore.EnqueueJob(job.Job{ - Dir: modHandle, - Func: func(ctx context.Context) error { - return ParseModuleManifest(w.fs, w.modStore, mod.Path) - }, - Type: op.OpTypeParseModuleManifest.String(), - Defer: decodeCalledModulesFunc(w.fs, w.modStore, w.schemaStore, w, mod.Path), - }) - if err == nil { - w.jobStore.WaitForJobs(ctx, id) - collectReferences(ctx, modHandle, w.modStore, w.schemaStore) - } - - return nil - } - if containsPath(mod.Watchable.PluginLockFiles, path) { - w.jobStore.EnqueueJob(job.Job{ - Dir: modHandle, - Func: func(ctx context.Context) error { - ctx = exec.WithExecutorFactory(ctx, w.tfExecFactory) - eo, ok := exec.ExecutorOptsFromContext(ctx) - if ok { - ctx = exec.WithExecutorOpts(ctx, eo) - } - - return ObtainSchema(ctx, w.modStore, w.schemaStore, mod.Path) - }, - Type: op.OpTypeObtainSchema.String(), - }) - w.jobStore.EnqueueJob(job.Job{ - Dir: modHandle, - Func: func(ctx context.Context) error { - ctx = exec.WithExecutorFactory(ctx, w.tfExecFactory) - eo, ok := exec.ExecutorOptsFromContext(ctx) - if ok { - ctx = exec.WithExecutorOpts(ctx, eo) - } - - return GetTerraformVersion(ctx, w.modStore, mod.Path) - }, - Type: op.OpTypeGetTerraformVersion.String(), - }) - return nil - } - return nil - }) - - return - } - - if containsPath(mod.Watchable.ModuleManifests, eventPath) { - id, err := w.jobStore.EnqueueJob(job.Job{ - Dir: modHandle, - Func: func(ctx context.Context) error { - return ParseModuleManifest(w.fs, w.modStore, mod.Path) - }, - Type: op.OpTypeParseModuleManifest.String(), - Defer: decodeCalledModulesFunc(w.fs, w.modStore, w.schemaStore, w, mod.Path), - }) - if err == nil { - w.jobStore.WaitForJobs(ctx, id) - collectReferences(ctx, modHandle, w.modStore, w.schemaStore) - } - return - } - - if containsPath(mod.Watchable.PluginLockFiles, eventPath) { - w.jobStore.EnqueueJob(job.Job{ - Dir: modHandle, - Func: func(jCtx context.Context) error { - ctx = exec.WithExecutorFactory(ctx, w.tfExecFactory) - eo, ok := exec.ExecutorOptsFromContext(ctx) - if ok { - ctx = exec.WithExecutorOpts(ctx, eo) - } - - return ObtainSchema(ctx, w.modStore, w.schemaStore, mod.Path) - }, - Type: op.OpTypeObtainSchema.String(), - }) - w.jobStore.EnqueueJob(job.Job{ - Dir: modHandle, - Func: func(ctx context.Context) error { - ctx = exec.WithExecutorFactory(ctx, w.tfExecFactory) - eo, ok := exec.ExecutorOptsFromContext(ctx) - if ok { - ctx = exec.WithExecutorOpts(ctx, eo) - } - - return GetTerraformVersion(ctx, w.modStore, mod.Path) - }, - Type: op.OpTypeGetTerraformVersion.String(), - }) - return - } - } - } - - if event.Op&fsnotify.Remove == fsnotify.Remove { - for modI, mod := range w.modules { - // Whole module being removed - if pathcmp.PathEquals(mod.Path, eventPath) { - for _, wPath := range mod.Watched { - w.fw.Remove(wPath) - } - w.fw.Remove(mod.Path) - w.modules = append(w.modules[:modI], w.modules[modI+1:]...) - return - } - - for i, wp := range mod.Watched { - if pathcmp.PathEquals(wp, eventPath) { - w.fw.Remove(wp) - mod.Watched = append(mod.Watched[:i], mod.Watched[i+1:]...) - return - } - } - } - } -} - -func decodeCalledModulesFunc(fs ReadOnlyFS, modStore *state.ModuleStore, schemaReader state.SchemaReader, w Watcher, modPath string) job.DeferFunc { - return func(ctx context.Context, opErr error) (jobIds job.IDs) { - if opErr != nil { - return - } - - moduleCalls, err := modStore.ModuleCalls(modPath) - if err != nil { - return - } - - jobStore, err := job.JobStoreFromContext(ctx) - if err != nil { - return - } - - // TODO: walk through declared modules too - maybe deduplicated? - - for _, mc := range moduleCalls.Installed { - fi, err := os.Stat(mc.Path) - if err != nil || !fi.IsDir() { - continue - } - modStore.Add(mc.Path) - - mcHandle := document.DirHandleFromPath(mc.Path) - // copy path for queued jobs below - mcPath := mc.Path - - id, err := jobStore.EnqueueJob(job.Job{ - Dir: mcHandle, - Func: func(ctx context.Context) error { - return ParseModuleConfiguration(fs, modStore, mcPath) - }, - Type: op.OpTypeParseModuleConfiguration.String(), - Defer: func(ctx context.Context, jobErr error) (ids job.IDs) { - id, err := jobStore.EnqueueJob(job.Job{ - Dir: mcHandle, - Type: op.OpTypeLoadModuleMetadata.String(), - Func: func(ctx context.Context) error { - return LoadModuleMetadata(modStore, mcPath) - }, - }) - if err != nil { - return - } - ids = append(ids, id) - - rIds := collectReferences(ctx, mcHandle, modStore, schemaReader) - ids = append(ids, rIds...) - - return - }, - }) - if err != nil { - return - } - jobIds = append(jobIds, id) - - id, err = jobStore.EnqueueJob(job.Job{ - Dir: mcHandle, - Func: func(ctx context.Context) error { - return ParseVariables(fs, modStore, mcPath) - }, - Type: op.OpTypeParseVariables.String(), - Defer: func(ctx context.Context, jobErr error) (ids job.IDs) { - id, err = jobStore.EnqueueJob(job.Job{ - Dir: mcHandle, - Func: func(ctx context.Context) error { - return DecodeVarsReferences(ctx, modStore, schemaReader, mcPath) - }, - Type: op.OpTypeDecodeVarsReferences.String(), - }) - if err != nil { - return - } - ids = append(ids, id) - return - }, - }) - if err != nil { - return - } - jobIds = append(jobIds, id) - - if w != nil { - w.AddModule(mc.Path) - } - } - - return - } -} - -func collectReferences(ctx context.Context, dirHandle document.DirHandle, modStore *state.ModuleStore, schemaReader state.SchemaReader) (ids job.IDs) { - jobStore, err := job.JobStoreFromContext(ctx) - if err != nil { - return - } - - id, err := jobStore.EnqueueJob(job.Job{ - Dir: dirHandle, - Func: func(ctx context.Context) error { - return DecodeReferenceTargets(ctx, modStore, schemaReader, dirHandle.Path()) - }, - Type: op.OpTypeDecodeReferenceTargets.String(), - }) - if err != nil { - return - } - ids = append(ids, id) - - id, err = jobStore.EnqueueJob(job.Job{ - Dir: dirHandle, - Func: func(ctx context.Context) error { - return DecodeReferenceOrigins(ctx, modStore, schemaReader, dirHandle.Path()) - }, - Type: op.OpTypeDecodeReferenceOrigins.String(), - }) - if err != nil { - return - } - ids = append(ids, id) - - return -} - -func containsPath(paths []string, path string) bool { - for _, p := range paths { - if pathcmp.PathEquals(p, path) { - return true - } - } - return false -} - -func (w *watcher) Start(ctx context.Context) error { - if w.watching { - w.logger.Println("watching already in progress") - return nil - } - - ctx, cancelFunc := context.WithCancel(ctx) - w.cancelFunc = cancelFunc - w.watching = true - - w.logger.Printf("watching for changes ...") - go w.run(ctx) - - return nil -} - -func (w *watcher) Stop() error { - if !w.watching { - return nil - } - - w.cancelFunc() - - err := w.fw.Close() - if err == nil { - w.watching = false - } - - return err -} diff --git a/internal/terraform/module/watcher_mock.go b/internal/terraform/module/watcher_mock.go deleted file mode 100644 index 5068750c..00000000 --- a/internal/terraform/module/watcher_mock.go +++ /dev/null @@ -1,39 +0,0 @@ -package module - -import ( - "context" - "log" - - "github.com/hashicorp/terraform-ls/internal/job" - "github.com/hashicorp/terraform-ls/internal/state" - "github.com/hashicorp/terraform-ls/internal/terraform/exec" -) - -func MockWatcher() WatcherFactory { - return func(fs ReadOnlyFS, ms *state.ModuleStore, pss *state.ProviderSchemaStore, js job.JobStore, tfExec exec.ExecutorFactory) (Watcher, error) { - return &mockWatcher{}, nil - } -} - -type mockWatcher struct{} - -func (w *mockWatcher) Start(context.Context) error { - return nil -} -func (w *mockWatcher) Stop() error { - return nil -} - -func (w *mockWatcher) SetLogger(*log.Logger) {} - -func (w *mockWatcher) AddModule(string) error { - return nil -} - -func (w *mockWatcher) RemoveModule(string) error { - return nil -} - -func (w *mockWatcher) IsModuleWatched(string) bool { - return false -} diff --git a/internal/terraform/module/watcher_test.go b/internal/terraform/module/watcher_test.go deleted file mode 100644 index 7e9bc168..00000000 --- a/internal/terraform/module/watcher_test.go +++ /dev/null @@ -1,197 +0,0 @@ -package module - -import ( - "context" - "io/ioutil" - "os" - "path/filepath" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/go-version" - "github.com/hashicorp/hcl-lang/schema" - "github.com/hashicorp/terraform-json" - "github.com/hashicorp/terraform-ls/internal/filesystem" - "github.com/hashicorp/terraform-ls/internal/job" - "github.com/hashicorp/terraform-ls/internal/scheduler" - "github.com/hashicorp/terraform-ls/internal/state" - "github.com/hashicorp/terraform-ls/internal/terraform/exec" - tfaddr "github.com/hashicorp/terraform-registry-address" - tfschema "github.com/hashicorp/terraform-schema/schema" - "github.com/stretchr/testify/mock" -) - -func TestWatcher_initFromScratch(t *testing.T) { - ss, err := state.NewStateStore() - if err != nil { - t.Fatal(err) - } - ss.SetLogger(testLogger()) - - fs := filesystem.NewFilesystem(ss.DocumentStore) - - modPath := filepath.Join(t.TempDir(), "module") - err = os.Mkdir(modPath, 0755) - if err != nil { - t.Fatal(err) - } - - psMock := &tfjson.ProviderSchemas{ - FormatVersion: "0.1", - Schemas: map[string]*tfjson.ProviderSchema{ - "registry.terraform.io/hashicorp/aws": {}, - }, - } - tfCalls := &exec.TerraformMockCalls{ - PerWorkDir: map[string][]*mock.Call{ - modPath: { - { - Method: "ProviderSchemas", - Arguments: []interface{}{ - mock.AnythingOfType("*context.valueCtx"), - }, - ReturnArguments: []interface{}{ - psMock, - nil, - }, - }, - { - Method: "Version", - Arguments: []interface{}{ - mock.AnythingOfType("*context.valueCtx"), - }, - ReturnArguments: []interface{}{ - version.Must(version.NewVersion("1.0.0")), - nil, - nil, - }, - }, - }, - }, - } - - ctx := context.Background() - - ctx = exec.WithExecutorOpts(ctx, &exec.ExecutorOpts{ - ExecPath: "tf-mock", - }) - - w, err := NewWatcher(fs, ss.Modules, ss.ProviderSchemas, ss.JobStore, exec.NewMockExecutor(tfCalls)) - if err != nil { - t.Fatal(err) - } - w.SetLogger(testLogger()) - - err = ss.Modules.Add(modPath) - if err != nil { - t.Fatal(err) - } - - b := []byte(` -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 3.0" - } - } -} - -provider "aws" { - region = "us-east-1" -} - -resource "aws_vpc" "example" { - cidr_block = "10.0.0.0/16" -} -`) - err = ioutil.WriteFile(filepath.Join(modPath, "main.tf"), b, 0755) - if err != nil { - t.Fatal(err) - } - - err = w.AddModule(modPath) - if err != nil { - t.Fatal(err) - } - - err = w.Start(ctx) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - w.Stop() - }) - - err = ioutil.WriteFile(filepath.Join(modPath, ".terraform.lock.hcl"), b, 0755) - if err != nil { - t.Fatal(err) - } - - // Give watcher some time to react - time.Sleep(250 * time.Millisecond) - - jobIds, err := ss.JobStore.ListQueuedJobs() - if err != nil { - t.Fatal(err) - } - t.Logf("queued jobs: %q", jobIds) - - scheduler := scheduler.NewScheduler(&closedJobStore{ss.JobStore}, 1) - scheduler.Start(ctx) - t.Cleanup(scheduler.Stop) - - err = ss.JobStore.WaitForJobs(ctx, jobIds...) - if err != nil { - t.Fatal(err) - } - - vc, err := version.NewConstraint("~> 3.0") - if err != nil { - t.Fatal(err) - } - ps, err := ss.ProviderSchemas.ProviderSchema(modPath, tfaddr.NewDefaultProvider("aws"), vc) - if err != nil { - t.Fatal(err) - } - expectedSchema := &tfschema.ProviderSchema{ - Resources: map[string]*schema.BodySchema{}, - DataSources: map[string]*schema.BodySchema{}, - } - if diff := cmp.Diff(expectedSchema, ps); diff != "" { - t.Fatalf("schema mismatch: %s", diff) - } - - mod, err := ss.Modules.ModuleByPath(modPath) - if err != nil { - t.Fatal(err) - } - if mod.TerraformVersion == nil { - t.Fatal("expected non-nil version") - } - if mod.TerraformVersion.String() != "1.0.0" { - t.Fatalf("version mismatch.\ngiven: %q\nexpected: %q", - mod.TerraformVersion.String(), "1.0.0") - } -} - -type closedJobStore struct { - js *state.JobStore -} - -func (js *closedJobStore) EnqueueJob(newJob job.Job) (job.ID, error) { - return js.js.EnqueueJob(newJob) -} - -func (js *closedJobStore) AwaitNextJob(ctx context.Context) (job.ID, job.Job, error) { - return js.js.AwaitNextJob(ctx, false) -} - -func (js *closedJobStore) FinishJob(id job.ID, jobErr error, deferredJobIds ...job.ID) error { - return js.js.FinishJob(id, jobErr, deferredJobIds...) -} - -func (js *closedJobStore) WaitForJobs(ctx context.Context, jobIds ...job.ID) error { - return js.js.WaitForJobs(ctx, jobIds...) -}