diff --git a/internal/indexer/walker.go b/internal/indexer/walker.go index 416bb12e3..abde683e0 100644 --- a/internal/indexer/walker.go +++ b/internal/indexer/walker.go @@ -17,6 +17,7 @@ func (idx *Indexer) WalkedModule(ctx context.Context, modHandle document.DirHand var errs *multierror.Error refCollectionDeps := make(job.IDs, 0) + providerVersionDeps := make(job.IDs, 0) parseId, err := idx.jobStore.EnqueueJob(job.Job{ Dir: modHandle, @@ -30,6 +31,7 @@ func (idx *Indexer) WalkedModule(ctx context.Context, modHandle document.DirHand } else { ids = append(ids, parseId) refCollectionDeps = append(refCollectionDeps, parseId) + providerVersionDeps = append(providerVersionDeps, parseId) } var metaId job.ID @@ -47,6 +49,7 @@ func (idx *Indexer) WalkedModule(ctx context.Context, modHandle document.DirHand } else { ids = append(ids, metaId) refCollectionDeps = append(refCollectionDeps, metaId) + providerVersionDeps = append(providerVersionDeps, metaId) } } @@ -98,41 +101,78 @@ func (idx *Indexer) WalkedModule(ctx context.Context, modHandle document.DirHand dataDir := datadir.WalkDataDirOfModule(idx.fs, modHandle.Path()) idx.logger.Printf("parsed datadir: %#v", dataDir) - if dataDir.PluginLockFilePath != "" { - pSchemaId, err := idx.jobStore.EnqueueJob(job.Job{ + var modManifestId job.ID + if dataDir.ModuleManifestPath != "" { + // References are collected *after* manifest parsing + // so that we reflect any references to submodules. + modManifestId, err = idx.jobStore.EnqueueJob(job.Job{ Dir: modHandle, Func: func(ctx context.Context) error { - ctx = exec.WithExecutorFactory(ctx, idx.tfExecFactory) - return module.ObtainSchema(ctx, idx.modStore, idx.schemaStore, modHandle.Path()) + return module.ParseModuleManifest(idx.fs, idx.modStore, modHandle.Path()) + }, + Type: op.OpTypeParseModuleManifest.String(), + Defer: func(ctx context.Context, jobErr error) (job.IDs, error) { + return idx.decodeInstalledModuleCalls(modHandle) }, - Type: op.OpTypeObtainSchema.String(), }) if err != nil { errs = multierror.Append(errs, err) } else { - ids = append(ids, pSchemaId) - refCollectionDeps = append(refCollectionDeps, pSchemaId) + ids = append(ids, modManifestId) + refCollectionDeps = append(refCollectionDeps, modManifestId) + // provider requirements may be within the (installed) modules + providerVersionDeps = append(providerVersionDeps, modManifestId) } } - if dataDir.ModuleManifestPath != "" { - // References are collected *after* manifest parsing - // so that we reflect any references to submodules. - modManifestId, err := idx.jobStore.EnqueueJob(job.Job{ + if dataDir.PluginLockFilePath != "" { + pSchemaId, err := idx.jobStore.EnqueueJob(job.Job{ Dir: modHandle, Func: func(ctx context.Context) error { - return module.ParseModuleManifest(idx.fs, idx.modStore, modHandle.Path()) + return module.ParseProviderVersions(idx.fs, idx.modStore, modHandle.Path()) }, - Type: op.OpTypeParseModuleManifest.String(), + Type: op.OpTypeParseProviderVersions.String(), + DependsOn: providerVersionDeps, Defer: func(ctx context.Context, jobErr error) (job.IDs, error) { - return idx.decodeInstalledModuleCalls(modHandle) + ids := make(job.IDs, 0) + + pReqs, err := idx.modStore.ProviderRequirementsForModule(modHandle.Path()) + if err != nil { + return ids, err + } + + exist, err := idx.schemaStore.AllSchemasExist(pReqs) + if err != nil { + return ids, err + } + if exist { + idx.logger.Printf("Avoiding obtaining schemas as they all exist: %#v", pReqs) + // avoid obtaining schemas if we already have it + return ids, nil + } + idx.logger.Printf("Obtaining schemas for: %#v", pReqs) + + id, err := idx.jobStore.EnqueueJob(job.Job{ + Dir: modHandle, + Func: func(ctx context.Context) error { + ctx = exec.WithExecutorFactory(ctx, idx.tfExecFactory) + return module.ObtainSchema(ctx, idx.modStore, idx.schemaStore, modHandle.Path()) + }, + Type: op.OpTypeObtainSchema.String(), + }) + if err != nil { + return ids, err + } + ids = append(ids, id) + + return ids, nil }, }) if err != nil { errs = multierror.Append(errs, err) } else { - ids = append(ids, modManifestId) - refCollectionDeps = append(refCollectionDeps, modManifestId) + ids = append(ids, pSchemaId) + refCollectionDeps = append(refCollectionDeps, pSchemaId) } } diff --git a/internal/indexer/watcher.go b/internal/indexer/watcher.go index 1e0ec202e..a2876d496 100644 --- a/internal/indexer/watcher.go +++ b/internal/indexer/watcher.go @@ -37,15 +37,46 @@ func (idx *Indexer) PluginLockChanged(ctx context.Context, modHandle document.Di 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 module.ParseProviderVersions(idx.fs, idx.modStore, modHandle.Path()) + }, + Defer: func(ctx context.Context, jobErr error) (job.IDs, error) { + ids := make(job.IDs, 0) + + mod, err := idx.modStore.ModuleByPath(modHandle.Path()) + if err != nil { + return ids, err + } + + exist, err := idx.schemaStore.AllSchemasExist(mod.Meta.ProviderRequirements) + if err != nil { + return ids, err + } + if exist { + // avoid obtaining schemas if we already have it + return ids, nil + } + + 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 module.ObtainSchema(ctx, idx.modStore, idx.schemaStore, modHandle.Path()) + }, + Type: op.OpTypeObtainSchema.String(), + }) + if err != nil { + return ids, err } + ids = append(ids, id) - return module.ObtainSchema(ctx, idx.modStore, idx.schemaStore, modHandle.Path()) + return ids, nil }, - Type: op.OpTypeObtainSchema.String(), + Type: op.OpTypeParseProviderVersions.String(), }) if err != nil { return ids, err diff --git a/internal/langserver/handlers/complete_test.go b/internal/langserver/handlers/complete_test.go index 6d938eeb7..c6ec84587 100644 --- a/internal/langserver/handlers/complete_test.go +++ b/internal/langserver/handlers/complete_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io/ioutil" "os" "path/filepath" "testing" @@ -45,8 +46,13 @@ func TestModuleCompletion_withValidData_basic(t *testing.T) { tmpDir := TempDir(t) InitPluginCache(t, tmpDir.Path()) + err := ioutil.WriteFile(filepath.Join(tmpDir.Path(), "main.tf"), []byte("provider \"test\" {\n\n}\n"), 0o755) + if err != nil { + t.Fatal(err) + } + var testSchema tfjson.ProviderSchemas - err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) + err = json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) if err != nil { t.Fatal(err) } @@ -259,9 +265,13 @@ func TestModuleCompletion_withValidData_basic(t *testing.T) { func TestModuleCompletion_withValidDataAndSnippets(t *testing.T) { tmpDir := TempDir(t) InitPluginCache(t, tmpDir.Path()) + err := ioutil.WriteFile(filepath.Join(tmpDir.Path(), "main.tf"), []byte("provider \"test\" {\n\n}\n"), 0o755) + if err != nil { + t.Fatal(err) + } var testSchema tfjson.ProviderSchemas - err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) + err = json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) if err != nil { t.Fatal(err) } diff --git a/internal/langserver/handlers/did_change_watched_files_test.go b/internal/langserver/handlers/did_change_watched_files_test.go index 9c2262a9f..be8a41165 100644 --- a/internal/langserver/handlers/did_change_watched_files_test.go +++ b/internal/langserver/handlers/did_change_watched_files_test.go @@ -701,7 +701,7 @@ func TestLangServer_DidChangeWatchedFiles_pluginChange(t *testing.T) { t.Fatal(err) } - originalTestDir := filepath.Join(testData, "uninitialized-single-submodule") + originalTestDir := filepath.Join(testData, "single-fake-provider") testDir := t.TempDir() // Copy test configuration so the test can run in isolation err = copy.Copy(originalTestDir, testDir) @@ -788,30 +788,6 @@ func TestLangServer_DidChangeWatchedFiles_pluginChange(t *testing.T) { t.Fatal("expected -/foo 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(`{ diff --git a/internal/langserver/handlers/document_link_test.go b/internal/langserver/handlers/document_link_test.go index 1d74179bf..b6232ca39 100644 --- a/internal/langserver/handlers/document_link_test.go +++ b/internal/langserver/handlers/document_link_test.go @@ -3,6 +3,8 @@ package handlers import ( "encoding/json" "fmt" + "io/ioutil" + "path/filepath" "testing" "github.com/hashicorp/go-version" @@ -17,9 +19,13 @@ import ( func TestDocumentLink_withValidData(t *testing.T) { tmpDir := TempDir(t) InitPluginCache(t, tmpDir.Path()) + err := ioutil.WriteFile(filepath.Join(tmpDir.Path(), "main.tf"), []byte("provider \"test\" {\n\n}\n"), 0o755) + if err != nil { + t.Fatal(err) + } var testSchema tfjson.ProviderSchemas - err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) + err = json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) if err != nil { t.Fatal(err) } diff --git a/internal/langserver/handlers/execute_command_module_providers_test.go b/internal/langserver/handlers/execute_command_module_providers_test.go index b4ef3e149..39642c0d4 100644 --- a/internal/langserver/handlers/execute_command_module_providers_test.go +++ b/internal/langserver/handlers/execute_command_module_providers_test.go @@ -97,7 +97,7 @@ func TestLangServer_workspaceExecuteCommand_moduleProviders_basic(t *testing.T) newDefaultProvider("aws"): version.Must(version.NewVersion("1.2.3")), newDefaultProvider("google"): version.Must(version.NewVersion("2.5.5")), } - err = s.Modules.UpdateInstalledProviders(modDir, pVersions) + err = s.Modules.UpdateInstalledProviders(modDir, pVersions, nil) if err != nil { t.Fatal(err) } diff --git a/internal/langserver/handlers/go_to_ref_target_test.go b/internal/langserver/handlers/go_to_ref_target_test.go index 8763cf367..f2765abb9 100644 --- a/internal/langserver/handlers/go_to_ref_target_test.go +++ b/internal/langserver/handlers/go_to_ref_target_test.go @@ -3,6 +3,7 @@ package handlers import ( "encoding/json" "fmt" + "io/ioutil" "path/filepath" "testing" @@ -118,8 +119,13 @@ func TestDefinition_withLinkToDefLessBlock(t *testing.T) { tmpDir := TempDir(t) InitPluginCache(t, tmpDir.Path()) + err := ioutil.WriteFile(filepath.Join(tmpDir.Path(), "main.tf"), []byte("provider \"test\" {\n\n}\n"), 0o755) + if err != nil { + t.Fatal(err) + } + var testSchema tfjson.ProviderSchemas - err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) + err = json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) if err != nil { t.Fatal(err) } @@ -266,8 +272,13 @@ func TestDefinition_withLinkToDefBlock(t *testing.T) { tmpDir := TempDir(t) InitPluginCache(t, tmpDir.Path()) + err := ioutil.WriteFile(filepath.Join(tmpDir.Path(), "main.tf"), []byte("provider \"test\" {\n\n}\n"), 0o755) + if err != nil { + t.Fatal(err) + } + var testSchema tfjson.ProviderSchemas - err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) + err = json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) if err != nil { t.Fatal(err) } @@ -598,8 +609,13 @@ func TestDeclaration_withLinkSupport(t *testing.T) { tmpDir := TempDir(t) InitPluginCache(t, tmpDir.Path()) + err := ioutil.WriteFile(filepath.Join(tmpDir.Path(), "main.tf"), []byte("provider \"test\" {\n\n}\n"), 0o755) + if err != nil { + t.Fatal(err) + } + var testSchema tfjson.ProviderSchemas - err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) + err = json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) if err != nil { t.Fatal(err) } diff --git a/internal/langserver/handlers/testdata/single-fake-provider/main.tf b/internal/langserver/handlers/testdata/single-fake-provider/main.tf new file mode 100644 index 000000000..08de6964a --- /dev/null +++ b/internal/langserver/handlers/testdata/single-fake-provider/main.tf @@ -0,0 +1,3 @@ +provider "foo" { + +} diff --git a/internal/state/module.go b/internal/state/module.go index 7f0158178..fb48d1d50 100644 --- a/internal/state/module.go +++ b/internal/state/module.go @@ -92,7 +92,9 @@ type Module struct { TerraformVersionErr error TerraformVersionState op.OpState - InstalledProviders InstalledProviders + InstalledProviders InstalledProviders + InstalledProvidersErr error + InstalledProvidersState op.OpState ProviderSchemaErr error ProviderSchemaState op.OpState @@ -143,6 +145,9 @@ func (m *Module) Copy() *Module { ProviderSchemaErr: m.ProviderSchemaErr, ProviderSchemaState: m.ProviderSchemaState, + InstalledProvidersErr: m.InstalledProvidersErr, + InstalledProvidersState: m.InstalledProvidersState, + RefTargets: m.RefTargets.Copy(), RefTargetsErr: m.RefTargetsErr, RefTargetsState: m.RefTargetsState, @@ -216,13 +221,14 @@ func (m *Module) Copy() *Module { func newModule(modPath string) *Module { return &Module{ - Path: modPath, - ModManifestState: op.OpStateUnknown, - TerraformVersionState: op.OpStateUnknown, - ProviderSchemaState: op.OpStateUnknown, - RefTargetsState: op.OpStateUnknown, - ModuleParsingState: op.OpStateUnknown, - MetaState: op.OpStateUnknown, + Path: modPath, + ModManifestState: op.OpStateUnknown, + TerraformVersionState: op.OpStateUnknown, + ProviderSchemaState: op.OpStateUnknown, + InstalledProvidersState: op.OpStateUnknown, + RefTargetsState: op.OpStateUnknown, + ModuleParsingState: op.OpStateUnknown, + MetaState: op.OpStateUnknown, } } @@ -385,6 +391,97 @@ func (s *ModuleStore) ModuleCalls(modPath string) (tfmod.ModuleCalls, error) { return modCalls, err } +func (s *ModuleStore) ProviderRequirementsForModule(modPath string) (tfmod.ProviderRequirements, error) { + return s.providerRequirementsForModule(modPath, 0) +} + +func (s *ModuleStore) providerRequirementsForModule(modPath string, level int) (tfmod.ProviderRequirements, error) { + // This is just a naive way of checking for cycles, so we don't end up + // crashing due to stack overflow. + // + // Cycles are however unlikely - at least for installed modules, since + // Terraform would return error when attempting to install modules + // with cycles. + if level > s.MaxModuleNesting { + return nil, fmt.Errorf("%s: too deep module nesting (%d)", modPath, s.MaxModuleNesting) + } + mod, err := s.ModuleByPath(modPath) + if err != nil { + return nil, err + } + + level++ + + requirements := make(tfmod.ProviderRequirements, 0) + for k, v := range mod.Meta.ProviderRequirements { + requirements[k] = v + } + + for _, mc := range mod.Meta.ModuleCalls { + localAddr, ok := mc.SourceAddr.(tfmod.LocalSourceAddr) + if !ok { + continue + } + + fullPath := filepath.Join(modPath, localAddr.String()) + + pr, err := s.providerRequirementsForModule(fullPath, level) + if err != nil { + return requirements, err + } + for pAddr, pCons := range pr { + if cons, ok := requirements[pAddr]; ok { + for _, c := range pCons { + if !constraintContains(cons, c) { + requirements[pAddr] = append(requirements[pAddr], c) + } + } + } + requirements[pAddr] = pCons + } + } + + if mod.ModManifest != nil { + for _, record := range mod.ModManifest.Records { + _, ok := record.SourceAddr.(tfmod.LocalSourceAddr) + if ok { + continue + } + + if record.IsRoot() { + continue + } + + fullPath := filepath.Join(modPath, record.Dir) + pr, err := s.providerRequirementsForModule(fullPath, level) + if err != nil { + continue + } + for pAddr, pCons := range pr { + if cons, ok := requirements[pAddr]; ok { + for _, c := range pCons { + if !constraintContains(cons, c) { + requirements[pAddr] = append(requirements[pAddr], c) + } + } + } + requirements[pAddr] = pCons + } + } + } + + return requirements, nil +} + +func constraintContains(vCons version.Constraints, cons *version.Constraint) bool { + for _, c := range vCons { + if c == cons { + return true + } + } + return false +} + func (s *ModuleStore) LocalModuleMeta(modPath string) (*tfmod.Meta, error) { mod, err := s.ModuleByPath(modPath) if err != nil { @@ -452,8 +549,11 @@ func moduleCopyByPath(txn *memdb.Txn, path string) (*Module, error) { return mod.Copy(), nil } -func (s *ModuleStore) UpdateInstalledProviders(path string, pvs map[tfaddr.Provider]*version.Version) error { +func (s *ModuleStore) UpdateInstalledProviders(path string, pvs map[tfaddr.Provider]*version.Version, pvErr error) error { txn := s.db.Txn(true) + txn.Defer(func() { + s.SetInstalledProvidersState(path, op.OpStateLoaded) + }) defer txn.Abort() oldMod, err := moduleByPath(txn, path) @@ -462,20 +562,8 @@ func (s *ModuleStore) UpdateInstalledProviders(path string, pvs map[tfaddr.Provi } mod := oldMod.Copy() - - // Providers may come from different sources (schema or version command) - // and we don't get their versions in both cases, so we make sure the existing - // versions are retained to get the most of both sources. - newProviders := make(map[tfaddr.Provider]*version.Version, 0) - for addr, pv := range pvs { - if pv == nil { - if v, ok := oldMod.InstalledProviders[addr]; ok && v != nil { - pv = v - } - } - newProviders[addr] = pv - } - mod.InstalledProviders = newProviders + mod.InstalledProviders = pvs + mod.InstalledProvidersErr = pvErr err = txn.Insert(s.tableName, mod) if err != nil { @@ -491,6 +579,26 @@ func (s *ModuleStore) UpdateInstalledProviders(path string, pvs map[tfaddr.Provi return nil } +func (s *ModuleStore) SetInstalledProvidersState(path string, state op.OpState) error { + txn := s.db.Txn(true) + defer txn.Abort() + + mod, err := moduleCopyByPath(txn, path) + if err != nil { + return err + } + + mod.InstalledProvidersState = state + + err = txn.Insert(s.tableName, mod) + if err != nil { + return err + } + + txn.Commit() + return nil +} + func (s *ModuleStore) List() ([]*Module, error) { txn := s.db.Txn(false) diff --git a/internal/state/module_test.go b/internal/state/module_test.go index 6d1eb0633..cc8f6b333 100644 --- a/internal/state/module_test.go +++ b/internal/state/module_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclparse" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/terraform/ast" "github.com/hashicorp/terraform-ls/internal/terraform/datadir" "github.com/hashicorp/terraform-ls/internal/terraform/module/operation" @@ -595,6 +596,100 @@ func TestModuleStore_UpdateVarsReferenceOrigins(t *testing.T) { } } +func TestProviderRequirementsForModule_cycle(t *testing.T) { + ss, err := NewStateStore() + if err != nil { + t.Fatal(err) + } + + ss.Modules.MaxModuleNesting = 3 + + modHandle := document.DirHandleFromPath(t.TempDir()) + meta := &tfmod.Meta{ + Path: modHandle.Path(), + ModuleCalls: map[string]tfmod.DeclaredModuleCall{ + "test": { + LocalName: "submod", + SourceAddr: tfmod.LocalSourceAddr("./"), + }, + }, + } + + err = ss.Modules.Add(modHandle.Path()) + if err != nil { + t.Fatal(err) + } + + err = ss.Modules.UpdateMetadata(modHandle.Path(), meta, nil) + if err != nil { + t.Fatal(err) + } + + _, err = ss.Modules.ProviderRequirementsForModule(modHandle.Path()) + if err == nil { + t.Fatal("expected error for cycle") + } +} + +func TestProviderRequirementsForModule_basic(t *testing.T) { + ss, err := NewStateStore() + if err != nil { + t.Fatal(err) + } + + // root module + modHandle := document.DirHandleFromPath(t.TempDir()) + meta := &tfmod.Meta{ + Path: modHandle.Path(), + ProviderRequirements: tfmod.ProviderRequirements{ + tfaddr.MustParseProviderSource("hashicorp/aws"): version.MustConstraints(version.NewConstraint(">= 1.0")), + }, + ModuleCalls: map[string]tfmod.DeclaredModuleCall{ + "test": { + LocalName: "submod", + SourceAddr: tfmod.LocalSourceAddr("./sub"), + }, + }, + } + err = ss.Modules.Add(modHandle.Path()) + if err != nil { + t.Fatal(err) + } + err = ss.Modules.UpdateMetadata(modHandle.Path(), meta, nil) + if err != nil { + t.Fatal(err) + } + + // submodule + submodHandle := document.DirHandleFromPath(filepath.Join(modHandle.Path(), "sub")) + subMeta := &tfmod.Meta{ + Path: modHandle.Path(), + ProviderRequirements: tfmod.ProviderRequirements{ + tfaddr.MustParseProviderSource("hashicorp/google"): version.MustConstraints(version.NewConstraint("> 2.0")), + }, + } + err = ss.Modules.Add(submodHandle.Path()) + if err != nil { + t.Fatal(err) + } + err = ss.Modules.UpdateMetadata(submodHandle.Path(), subMeta, nil) + if err != nil { + t.Fatal(err) + } + + expectedReqs := tfmod.ProviderRequirements{ + tfaddr.MustParseProviderSource("hashicorp/aws"): version.MustConstraints(version.NewConstraint(">= 1.0")), + tfaddr.MustParseProviderSource("hashicorp/google"): version.MustConstraints(version.NewConstraint("> 2.0")), + } + pReqs, err := ss.Modules.ProviderRequirementsForModule(modHandle.Path()) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(expectedReqs, pReqs, cmpOpts); diff != "" { + t.Fatalf("unexpected requirements: %s", diff) + } +} + func BenchmarkModuleByPath(b *testing.B) { s, err := NewStateStore() if err != nil { diff --git a/internal/state/provider_schema.go b/internal/state/provider_schema.go index 07712cbad..c59b22f71 100644 --- a/internal/state/provider_schema.go +++ b/internal/state/provider_schema.go @@ -192,6 +192,64 @@ func (s *ProviderSchemaStore) AddPreloadedSchema(addr tfaddr.Provider, pv *versi return nil } +func (s *ProviderSchemaStore) AllSchemasExist(pvm map[tfaddr.Provider]version.Constraints) (bool, error) { + for pAddr, pCons := range pvm { + exists, err := s.schemaExists(pAddr, pCons) + if err != nil { + return false, err + } + if !exists { + return false, nil + } + } + + return true, nil +} + +func (s *ProviderSchemaStore) schemaExists(addr tfaddr.Provider, pCons version.Constraints) (bool, error) { + txn := s.db.Txn(false) + + it, err := txn.Get(s.tableName, "id_prefix", addr) + if err != nil { + return false, err + } + + for item := it.Next(); item != nil; item = it.Next() { + ps, ok := item.(*ProviderSchema) + if ok { + if ps.Schema == nil { + // Incomplete entry may be a result of provider version being + // sourced earlier where schema is yet to be sourced or sourcing failed. + continue + } + } + + if providerAddrEquals(ps.Address, addr) && pCons.Check(ps.Version) { + return true, nil + } + } + + return false, nil +} + +func providerAddrEquals(a, b tfaddr.Provider) bool { + if a.Equals(b) { + return true + } + + // Account for legacy addresses which may come from Terraform + // 0.12 or 0.13 running locally or just lack of required_providers + // entry in configuration. + if a.IsLegacy() { + a.Namespace = "hashicorp" + } + if b.IsLegacy() { + b.Namespace = "hashicorp" + } + + return a.Equals(b) +} + func (s *ProviderSchemaStore) ProviderSchema(modPath string, addr tfaddr.Provider, vc version.Constraints) (*tfschema.ProviderSchema, error) { s.logger.Printf("PSS: getting provider schema (%s, %s, %s)", modPath, addr, vc) txn := s.db.Txn(false) diff --git a/internal/state/provider_schema_test.go b/internal/state/provider_schema_test.go index f1ec9adf8..a21e2eae1 100644 --- a/internal/state/provider_schema_test.go +++ b/internal/state/provider_schema_test.go @@ -819,6 +819,99 @@ func TestStateStore_ListSchemas(t *testing.T) { } } +func TestAllSchemasExist(t *testing.T) { + testCases := []struct { + Name string + Requirements map[tfaddr.Provider]version.Constraints + InstalledProviders InstalledProviders + ExpectedMatch bool + ExpectedErr bool + }{ + { + Name: "empty requirements", + Requirements: map[tfaddr.Provider]version.Constraints{}, + InstalledProviders: InstalledProviders{}, + ExpectedMatch: true, + ExpectedErr: false, + }, + { + Name: "missing all installed providers", + Requirements: map[tfaddr.Provider]version.Constraints{ + tfaddr.MustParseProviderSource("hashicorp/test"): version.MustConstraints(version.NewConstraint("1.0.0")), + }, + InstalledProviders: InstalledProviders{}, + ExpectedMatch: false, + ExpectedErr: false, + }, + { + Name: "missing one of two installed providers", + Requirements: map[tfaddr.Provider]version.Constraints{ + tfaddr.MustParseProviderSource("hashicorp/aws"): version.MustConstraints(version.NewConstraint(">= 1.0.0")), + tfaddr.MustParseProviderSource("hashicorp/google"): version.MustConstraints(version.NewConstraint(">= 1.0.0")), + }, + InstalledProviders: InstalledProviders{ + tfaddr.MustParseProviderSource("hashicorp/aws"): version.Must(version.NewVersion("1.0.0")), + }, + ExpectedMatch: false, + ExpectedErr: false, + }, + { + Name: "missing installed provider version", + Requirements: map[tfaddr.Provider]version.Constraints{ + tfaddr.MustParseProviderSource("hashicorp/aws"): version.MustConstraints(version.NewConstraint(">= 1.0.0")), + }, + InstalledProviders: InstalledProviders{ + tfaddr.MustParseProviderSource("hashicorp/aws"): version.Must(version.NewVersion("0.1.0")), + }, + ExpectedMatch: false, + ExpectedErr: false, + }, + { + Name: "matching installed providers", + Requirements: map[tfaddr.Provider]version.Constraints{ + tfaddr.MustParseProviderSource("hashicorp/test"): version.MustConstraints(version.NewConstraint("1.0.0")), + }, + InstalledProviders: InstalledProviders{ + tfaddr.MustParseProviderSource("hashicorp/test"): version.Must(version.NewVersion("1.0.0")), + }, + ExpectedMatch: true, + ExpectedErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + ss, err := NewStateStore() + if err != nil { + t.Fatal(err) + } + + for pAddr, pVersion := range tc.InstalledProviders { + err = ss.ProviderSchemas.AddPreloadedSchema(pAddr, pVersion, &tfschema.ProviderSchema{}) + if err != nil { + t.Fatal(err) + } + } + + exist, err := ss.ProviderSchemas.AllSchemasExist(tc.Requirements) + if err != nil && !tc.ExpectedErr { + t.Fatal(err) + } + if err == nil && tc.ExpectedErr { + t.Fatal("expected error") + } + if exist && !tc.ExpectedMatch { + t.Fatalf("expected schemas mismatch\nrequirements: %v\ninstalled: %v\n", + tc.Requirements, tc.InstalledProviders) + } + if !exist && tc.ExpectedMatch { + t.Fatalf("expected schemas match\nrequirements: %v\ninstalled: %v\n", + tc.Requirements, tc.InstalledProviders) + } + }) + } +} + // BenchmarkProviderSchema exercises the hot path for most handlers which require schema func BenchmarkProviderSchema(b *testing.B) { s, err := NewStateStore() diff --git a/internal/state/state.go b/internal/state/state.go index db1a67eaa..bd290c0d0 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -234,6 +234,10 @@ type ModuleStore struct { // TimeProvider provides current time (for mocking time.Now in tests) TimeProvider func() time.Time + + // MaxModuleNesting represents how many nesting levels we'd attempt + // to parse provider requirements before returning error. + MaxModuleNesting int } type ModuleChangeStore struct { @@ -289,10 +293,11 @@ func NewStateStore() (*StateStore, error) { nextJobLowPrioMu: &sync.Mutex{}, }, Modules: &ModuleStore{ - db: db, - tableName: moduleTableName, - logger: defaultLogger, - TimeProvider: time.Now, + db: db, + tableName: moduleTableName, + logger: defaultLogger, + TimeProvider: time.Now, + MaxModuleNesting: 50, }, ProviderSchemas: &ProviderSchemaStore{ db: db, diff --git a/internal/terraform/datadir/module_manifest.go b/internal/terraform/datadir/module_manifest.go index 790d1e095..eafdac6af 100644 --- a/internal/terraform/datadir/module_manifest.go +++ b/internal/terraform/datadir/module_manifest.go @@ -13,6 +13,10 @@ import ( tfmod "github.com/hashicorp/terraform-schema/module" ) +var manifestPathElements = []string{ + DataDirName, "modules", "modules.json", +} + func ModuleManifestFilePath(fs fs.StatFS, modulePath string) (string, bool) { manifestPath := filepath.Join( append([]string{modulePath}, diff --git a/internal/terraform/datadir/paths.go b/internal/terraform/datadir/paths.go index d180954dc..cf6434f76 100644 --- a/internal/terraform/datadir/paths.go +++ b/internal/terraform/datadir/paths.go @@ -9,19 +9,6 @@ import ( const DataDirName = ".terraform" -var pluginLockFilePathElements = [][]string{ - // Terraform >= 0.14 - {".terraform.lock.hcl"}, - // Terraform >= v0.13 - {DataDirName, "plugins", "selections.json"}, - // Terraform >= v0.12 - {DataDirName, "plugins", runtime.GOOS + "_" + runtime.GOARCH, "lock.json"}, -} - -var manifestPathElements = []string{ - DataDirName, "modules", "modules.json", -} - func watchableModuleDirs(modPath string) []string { return []string{ filepath.Join(modPath, DataDirName), diff --git a/internal/terraform/datadir/plugin_lock_file.go b/internal/terraform/datadir/plugin_lock_file.go index 4bc73d521..54e8a9fcd 100644 --- a/internal/terraform/datadir/plugin_lock_file.go +++ b/internal/terraform/datadir/plugin_lock_file.go @@ -1,10 +1,29 @@ package datadir import ( + "encoding/json" + "errors" "io/fs" "path/filepath" + "regexp" + "runtime" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + tfaddr "github.com/hashicorp/terraform-registry-address" + "github.com/zclconf/go-cty/cty" ) +var pluginLockFilePathElements = [][]string{ + // Terraform >= 0.14 + {".terraform.lock.hcl"}, + // Terraform >= v0.13 + {DataDirName, "plugins", "selections.json"}, + // Terraform >= v0.12 + {DataDirName, "plugins", runtime.GOOS + "_" + runtime.GOARCH, "lock.json"}, +} + func PluginLockFilePath(fs fs.StatFS, modPath string) (string, bool) { for _, pathElems := range pluginLockFilePathElements { fullPath := filepath.Join(append([]string{modPath}, pathElems...)...) @@ -16,3 +35,182 @@ func PluginLockFilePath(fs fs.StatFS, modPath string) (string, bool) { return "", false } + +type PluginVersionMap map[tfaddr.Provider]*version.Version + +type FS interface { + ReadFile(name string) ([]byte, error) + ReadDir(name string) ([]fs.DirEntry, error) +} + +func ParsePluginVersions(filesystem FS, modPath string) (PluginVersionMap, error) { + pvm, err := parsePluginLockFile_v014(filesystem, modPath) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return nil, err + } + } else { + return pvm, nil + } + + pvm, err = parsePluginLockFile_v013(filesystem, modPath) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return nil, err + } + } else { + return pvm, nil + } + + return parsePluginDir_v012(filesystem, modPath) +} + +// parsePluginDir_v012 parses the 0.12-style datadir. +// See https://github.com/hashicorp/terraform/blob/v0.12.0/plugin/discovery/find.go#L45 +func parsePluginDir_v012(filesystem FS, modPath string) (PluginVersionMap, error) { + // Unfortunately the lock.json from 0.12 only contains hashes, not versions + // so we have to imply the versions from filenames (which is what Terraform 0.12 does too) + dirPath := filepath.Join(modPath, DataDirName, "plugins", runtime.GOOS+"_"+runtime.GOARCH) + entries, err := filesystem.ReadDir(dirPath) + if err != nil { + return nil, err + } + + // terraform-provider-aws_v4.23.0_x5 + filenameRe := regexp.MustCompile(`^terraform-provider-([^_]+)_(v[^_]+)`) + + pvm := make(PluginVersionMap, 0) + for _, entry := range entries { + name := entry.Name() + + matches := filenameRe.FindStringSubmatch(name) + if len(matches) != 3 { + continue + } + + providerName, err := tfaddr.ParseProviderPart(matches[1]) + if err != nil { + continue + } + providerVersion, err := version.NewVersion(matches[2]) + if err != nil { + continue + } + + pvm[legacyProviderAddr(providerName)] = providerVersion + } + + return pvm, nil +} + +func legacyProviderAddr(name string) tfaddr.Provider { + return tfaddr.Provider{ + Hostname: tfaddr.DefaultProviderRegistryHost, + Namespace: tfaddr.LegacyProviderNamespace, + Type: name, + } +} + +func parsePluginLockFile_v013(filesystem FS, modPath string) (PluginVersionMap, error) { + fullPath := filepath.Join(modPath, DataDirName, "plugins", "selections.json") + + src, err := filesystem.ReadFile(fullPath) + if err != nil { + return nil, err + } + + file := selectionFile{} + err = json.Unmarshal(src, &file) + if err != nil { + return nil, err + } + + pvm := make(PluginVersionMap, 0) + for rawAddress, sel := range file { + pAddr, err := tfaddr.ParseProviderSource(rawAddress) + if err != nil { + continue + } + pvm[pAddr] = sel.Version + } + + return pvm, nil +} + +type selectionFile map[string]selection + +type selection struct { + Version *version.Version +} + +func parsePluginLockFile_v014(filesystem FS, modPath string) (PluginVersionMap, error) { + fullPath := filepath.Join(modPath, ".terraform.lock.hcl") + + src, err := filesystem.ReadFile(fullPath) + if err != nil { + return nil, err + } + + cfg, diags := hclsyntax.ParseConfig(src, ".terraform.lock.hcl", hcl.InitialPos) + if diags.HasErrors() { + return nil, diags + } + + // We precautiosly use PartialContent, to avoid this breaking + // in case Terraform CLI introduces new blocks + body, _, diags := cfg.Body.PartialContent(lockFileSchema) + if diags.HasErrors() { + return nil, diags + } + + pvm := make(PluginVersionMap, 0) + for _, block := range body.Blocks.OfType("provider") { + if len(block.Labels) != 1 { + continue + } + + pAddr, err := tfaddr.ParseProviderSource(block.Labels[0]) + if err != nil { + continue + } + + pBody, _, diags := block.Body.PartialContent(providerSchema) + if diags.HasErrors() { + continue + } + + val, diags := pBody.Attributes["version"].Expr.Value(nil) + if diags.HasErrors() { + continue + } + if val.Type() != cty.String { + continue + } + pVersion, err := version.NewVersion(val.AsString()) + if err != nil { + continue + } + + pvm[pAddr] = pVersion + } + + return pvm, nil +} + +var lockFileSchema = &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "provider", + LabelNames: []string{"source"}, + }, + }, +} + +var providerSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "version", + Required: true, + }, + }, +} diff --git a/internal/terraform/datadir/plugin_lock_file_test.go b/internal/terraform/datadir/plugin_lock_file_test.go new file mode 100644 index 000000000..13afecbf0 --- /dev/null +++ b/internal/terraform/datadir/plugin_lock_file_test.go @@ -0,0 +1,131 @@ +package datadir + +import ( + "io/fs" + "path/filepath" + "runtime" + "testing" + "testing/fstest" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-version" + tfaddr "github.com/hashicorp/terraform-registry-address" +) + +func TestParsePluginVersions_basic012(t *testing.T) { + // TODO: Replace OS-specific separator with '/' + // See https://github.com/hashicorp/terraform-ls/issues/1025 + fs := fstest.MapFS{ + "foo-module": &fstest.MapFile{Mode: fs.ModeDir}, + filepath.Join("foo-module", ".terraform"): &fstest.MapFile{Mode: fs.ModeDir}, + filepath.Join("foo-module", ".terraform", "plugins"): &fstest.MapFile{Mode: fs.ModeDir}, + filepath.Join("foo-module", ".terraform", "plugins", runtime.GOOS+"_"+runtime.GOARCH): &fstest.MapFile{Mode: fs.ModeDir}, + filepath.Join("foo-module", ".terraform", "plugins", runtime.GOOS+"_"+runtime.GOARCH) + "/terraform-provider-aws_v4.23.0_x5": &fstest.MapFile{}, + filepath.Join("foo-module", ".terraform", "plugins", runtime.GOOS+"_"+runtime.GOARCH) + "/terraform-provider-google_v4.29.0_x5": &fstest.MapFile{}, + } + expectedVersions := PluginVersionMap{ + legacyProviderAddr("aws"): version.Must(version.NewVersion("4.23.0")), + legacyProviderAddr("google"): version.Must(version.NewVersion("4.29.0")), + } + versions, err := ParsePluginVersions(fs, "foo-module") + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(expectedVersions, versions); diff != "" { + t.Fatalf("unexpected versions: %s", diff) + } +} + +func TestParsePluginVersions_basic013(t *testing.T) { + fs := fstest.MapFS{ + "foo-module": &fstest.MapFile{Mode: fs.ModeDir}, + filepath.Join("foo-module", ".terraform"): &fstest.MapFile{Mode: fs.ModeDir}, + filepath.Join("foo-module", ".terraform", "plugins"): &fstest.MapFile{Mode: fs.ModeDir}, + filepath.Join("foo-module", ".terraform", "plugins", "selections.json"): &fstest.MapFile{ + Data: []byte(`{ + "registry.terraform.io/hashicorp/aws": { + "hash": "h1:j6RGCfnoLBpzQVOKUbGyxf4EJtRvQClKplO+WdXL5O0=", + "version": "4.23.0" + }, + "registry.terraform.io/hashicorp/google": { + "hash": "h1:vZdocusWLMUSeRLI3W3dd3bgKYovGntsaHiXFIfM484=", + "version": "4.29.0" + } +}`), + }, + } + expectedVersions := PluginVersionMap{ + tfaddr.MustParseProviderSource("hashicorp/aws"): version.Must(version.NewVersion("4.23.0")), + tfaddr.MustParseProviderSource("hashicorp/google"): version.Must(version.NewVersion("4.29.0")), + } + versions, err := ParsePluginVersions(fs, "foo-module") + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(expectedVersions, versions); diff != "" { + t.Fatalf("unexpected versions: %s", diff) + } +} + +func TestParsePluginVersions_basic014(t *testing.T) { + fs := fstest.MapFS{ + "foo-module": &fstest.MapFile{Mode: fs.ModeDir}, + filepath.Join("foo-module", ".terraform.lock.hcl"): &fstest.MapFile{ + Data: []byte(`# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "4.23.0" + hashes = [ + "h1:j6RGCfnoLBpzQVOKUbGyxf4EJtRvQClKplO+WdXL5O0=", + "zh:17adbedc9a80afc571a8de7b9bfccbe2359e2b3ce1fffd02b456d92248ec9294", + "zh:23d8956b031d78466de82a3d2bbe8c76cc58482c931af311580b8eaef4e6a38f", + "zh:343fe19e9a9f3021e26f4af68ff7f4828582070f986b6e5e5b23d89df5514643", + "zh:6b8ff83d884b161939b90a18a4da43dd464c4b984f54b5f537b2870ce6bd94bc", + "zh:7777d614d5e9d589ad5508eecf4c6d8f47d50fcbaf5d40fa7921064240a6b440", + "zh:82f4578861a6fd0cde9a04a1926920bd72d993d524e5b34d7738d4eff3634c44", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a08fefc153bbe0586389e814979cf7185c50fcddbb2082725991ed02742e7d1e", + "zh:ae789c0e7cb777d98934387f8888090ccb2d8973ef10e5ece541e8b624e1fb00", + "zh:b4608aab78b4dbb32c629595797107fc5a84d1b8f0682f183793d13837f0ecf0", + "zh:ed2c791c2354764b565f9ba4be7fc845c619c1a32cefadd3154a5665b312ab00", + "zh:f94ac0072a8545eebabf417bc0acbdc77c31c006ad8760834ee8ee5cdb64e743", + ] +} + +provider "registry.terraform.io/hashicorp/google" { + version = "4.29.0" + hashes = [ + "h1:vZdocusWLMUSeRLI3W3dd3bgKYovGntsaHiXFIfM484=", + "zh:00ac3a2c7006d349147809961839be1ceda83d5c620aa30541064e2507b72f35", + "zh:1602bdc71667abfbcc34c15944decabc5e05e167e49ce4045dc13ba234a27995", + "zh:173c2fb837c9c1a9b103ca9f9ade456effc705a5539ddab2a7de0b1e3d59af73", + "zh:231c28cc9698c9ce87218f9a8073dd30aa51b97511bf57e533b7780581cb2e4f", + "zh:2423c1f8065b309fc7340b880fa898f877e715c734b5322c12d004335c7591d4", + "zh:2c0d650520e32d8d884a4fb83cf3527605a8cadab557a0857290a3b14b85f6e5", + "zh:8ef536b0cb362a377e058c4105d4748cd7c4b083376abc829ce8d66396c589c7", + "zh:9da3e2987cd737b843f0a8558b400af1f0fe60929cd23788800a1114818d982d", + "zh:ad727c5eba4cce83a44f3747637876462686465e64ac40099a084935a538bb57", + "zh:b3895af9e06d0142ef5c6bbdd8dd0b2acb4dffa9c6631b9b6b984719c157cc1b", + "zh:d7be31e59a254f952f4e03bedbf4dfbd6717f5e9e5d31e1add52711f6da4aedb", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} +`), + }, + } + expectedVersions := PluginVersionMap{ + tfaddr.MustParseProviderSource("hashicorp/aws"): version.Must(version.NewVersion("4.23.0")), + tfaddr.MustParseProviderSource("hashicorp/google"): version.Must(version.NewVersion("4.29.0")), + } + versions, err := ParsePluginVersions(fs, "foo-module") + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(expectedVersions, versions); diff != "" { + t.Fatalf("unexpected versions: %s", diff) + } +} diff --git a/internal/terraform/module/module_ops.go b/internal/terraform/module/module_ops.go index 876863be8..8b672eab0 100644 --- a/internal/terraform/module/module_ops.go +++ b/internal/terraform/module/module_ops.go @@ -74,22 +74,17 @@ func GetTerraformVersion(ctx context.Context, modStore *state.ModuleStore, modPa } v, pv, err := tfExec.Version(ctx) - pVersions := providerVersions(pv) + pVersions := providerVersionsFromTfVersion(pv) sErr := modStore.UpdateTerraformVersion(modPath, v, pVersions, err) if sErr != nil { return sErr } - ipErr := modStore.UpdateInstalledProviders(modPath, pVersions) - if ipErr != nil { - return ipErr - } - return err } -func providerVersions(pv map[string]*version.Version) map[tfaddr.Provider]*version.Version { +func providerVersionsFromTfVersion(pv map[string]*version.Version) map[tfaddr.Provider]*version.Version { m := make(map[tfaddr.Provider]*version.Version, 0) for rawAddr, v := range pv { @@ -131,8 +126,6 @@ func ObtainSchema(ctx context.Context, modStore *state.ModuleStore, schemaStore return err } - installedProviders := make(map[tfaddr.Provider]*version.Version, 0) - for rawAddr, pJsonSchema := range ps.Schemas { pAddr, err := tfaddr.ParseProviderSource(rawAddr) if err != nil { @@ -140,8 +133,6 @@ func ObtainSchema(ctx context.Context, modStore *state.ModuleStore, schemaStore continue } - installedProviders[pAddr] = nil - if pAddr.IsLegacy() { // TODO: check for migrations via Registry API? } @@ -154,7 +145,7 @@ func ObtainSchema(ctx context.Context, modStore *state.ModuleStore, schemaStore } } - return modStore.UpdateInstalledProviders(modPath, installedProviders) + return nil } func ParseModuleConfiguration(fs ReadOnlyFS, modStore *state.ModuleStore, modPath string) error { @@ -233,6 +224,22 @@ func ParseModuleManifest(fs ReadOnlyFS, modStore *state.ModuleStore, modPath str return err } +func ParseProviderVersions(fs ReadOnlyFS, modStore *state.ModuleStore, modPath string) error { + err := modStore.SetInstalledProvidersState(modPath, op.OpStateLoading) + if err != nil { + return err + } + + pvm, err := datadir.ParsePluginVersions(fs, modPath) + + sErr := modStore.UpdateInstalledProviders(modPath, pvm, err) + if sErr != nil { + return sErr + } + + return err +} + func LoadModuleMetadata(modStore *state.ModuleStore, modPath string) error { err := modStore.SetMetaState(modPath, op.OpStateLoading) if err != nil { diff --git a/internal/terraform/module/module_ops_test.go b/internal/terraform/module/module_ops_test.go index 4867a38d6..7bac65f80 100644 --- a/internal/terraform/module/module_ops_test.go +++ b/internal/terraform/module/module_ops_test.go @@ -3,10 +3,12 @@ package module import ( "context" "fmt" + "io/fs" "net/http" "net/http/httptest" "path/filepath" "testing" + "testing/fstest" "time" "github.com/google/go-cmp/cmp" @@ -15,6 +17,7 @@ import ( "github.com/hashicorp/terraform-ls/internal/filesystem" "github.com/hashicorp/terraform-ls/internal/registry" "github.com/hashicorp/terraform-ls/internal/state" + "github.com/hashicorp/terraform-ls/internal/terraform/module/operation" tfaddr "github.com/hashicorp/terraform-registry-address" tfregistry "github.com/hashicorp/terraform-schema/registry" "github.com/zclconf/go-cty-debug/ctydebug" @@ -326,3 +329,62 @@ var expectedModuleData = &tfregistry.ModuleData{ }, }, } + +func TestParseProviderVersions(t *testing.T) { + modPath := "testdir" + + fs := fstest.MapFS{ + modPath: &fstest.MapFile{Mode: fs.ModeDir}, + filepath.Join(modPath, ".terraform.lock.hcl"): &fstest.MapFile{ + Data: []byte(`provider "registry.terraform.io/hashicorp/aws" { + version = "4.23.0" + hashes = [ + "h1:j6RGCfnoLBpzQVOKUbGyxf4EJtRvQClKplO+WdXL5O0=", + "zh:17adbedc9a80afc571a8de7b9bfccbe2359e2b3ce1fffd02b456d92248ec9294", + "zh:23d8956b031d78466de82a3d2bbe8c76cc58482c931af311580b8eaef4e6a38f", + "zh:343fe19e9a9f3021e26f4af68ff7f4828582070f986b6e5e5b23d89df5514643", + "zh:6b8ff83d884b161939b90a18a4da43dd464c4b984f54b5f537b2870ce6bd94bc", + "zh:7777d614d5e9d589ad5508eecf4c6d8f47d50fcbaf5d40fa7921064240a6b440", + "zh:82f4578861a6fd0cde9a04a1926920bd72d993d524e5b34d7738d4eff3634c44", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a08fefc153bbe0586389e814979cf7185c50fcddbb2082725991ed02742e7d1e", + "zh:ae789c0e7cb777d98934387f8888090ccb2d8973ef10e5ece541e8b624e1fb00", + "zh:b4608aab78b4dbb32c629595797107fc5a84d1b8f0682f183793d13837f0ecf0", + "zh:ed2c791c2354764b565f9ba4be7fc845c619c1a32cefadd3154a5665b312ab00", + "zh:f94ac0072a8545eebabf417bc0acbdc77c31c006ad8760834ee8ee5cdb64e743", + ] +} +`), + }, + } + + ss, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + + err = ss.Modules.Add(modPath) + if err != nil { + t.Fatal(err) + } + + err = ParseProviderVersions(fs, ss.Modules, modPath) + if err != nil { + t.Fatal(err) + } + + mod, err := ss.Modules.ModuleByPath(modPath) + if err != nil { + t.Fatal(err) + } + + if mod.InstalledProvidersState != operation.OpStateLoaded { + t.Fatalf("expected state to be loaded, %q given", mod.InstalledProvidersState) + } + expectedInstalledProviders := state.InstalledProviders{ + tfaddr.MustParseProviderSource("hashicorp/aws"): version.Must(version.NewVersion("4.23.0")), + } + if diff := cmp.Diff(expectedInstalledProviders, mod.InstalledProviders); diff != "" { + t.Fatalf("unexpected providers: %s", diff) + } +} diff --git a/internal/terraform/module/operation/op_type_string.go b/internal/terraform/module/operation/op_type_string.go index 3f2eb1c18..14b82b1b1 100644 --- a/internal/terraform/module/operation/op_type_string.go +++ b/internal/terraform/module/operation/op_type_string.go @@ -19,11 +19,12 @@ func _() { _ = x[OpTypeDecodeReferenceOrigins-8] _ = x[OpTypeDecodeVarsReferences-9] _ = x[OpTypeGetModuleDataFromRegistry-10] + _ = x[OpTypeParseProviderVersions-11] } -const _OpType_name = "OpTypeUnknownOpTypeGetTerraformVersionOpTypeObtainSchemaOpTypeParseModuleConfigurationOpTypeParseVariablesOpTypeParseModuleManifestOpTypeLoadModuleMetadataOpTypeDecodeReferenceTargetsOpTypeDecodeReferenceOriginsOpTypeDecodeVarsReferencesOpTypeGetModuleDataFromRegistry" +const _OpType_name = "OpTypeUnknownOpTypeGetTerraformVersionOpTypeObtainSchemaOpTypeParseModuleConfigurationOpTypeParseVariablesOpTypeParseModuleManifestOpTypeLoadModuleMetadataOpTypeDecodeReferenceTargetsOpTypeDecodeReferenceOriginsOpTypeDecodeVarsReferencesOpTypeGetModuleDataFromRegistryOpTypeParseProviderVersions" -var _OpType_index = [...]uint16{0, 13, 38, 56, 86, 106, 131, 155, 183, 211, 237, 268} +var _OpType_index = [...]uint16{0, 13, 38, 56, 86, 106, 131, 155, 183, 211, 237, 268, 295} func (i OpType) String() string { if i >= OpType(len(_OpType_index)-1) { diff --git a/internal/terraform/module/operation/operation.go b/internal/terraform/module/operation/operation.go index 5f0919f38..36f85a101 100644 --- a/internal/terraform/module/operation/operation.go +++ b/internal/terraform/module/operation/operation.go @@ -25,4 +25,5 @@ const ( OpTypeDecodeReferenceOrigins OpTypeDecodeVarsReferences OpTypeGetModuleDataFromRegistry + OpTypeParseProviderVersions ) diff --git a/internal/walker/walker_test.go b/internal/walker/walker_test.go index 378d57d79..6161e6948 100644 --- a/internal/walker/walker_test.go +++ b/internal/walker/walker_test.go @@ -387,6 +387,7 @@ func TestWalker_complexModules(t *testing.T) { pa := state.NewPathAwaiter(ss.WalkerPaths, false) indexer := indexer.NewIndexer(fs, ss.Modules, ss.ProviderSchemas, ss.RegistryModules, ss.JobStore, exec.NewMockExecutor(tfCalls), registry.NewClient()) + indexer.SetLogger(testLogger()) w := NewWalker(fs, pa, ss.Modules, indexer.WalkedModule) w.Collector = NewWalkerCollector() w.SetLogger(testLogger())