diff --git a/schema/schema_merge.go b/schema/schema_merge.go index bc1b93da..d117433c 100644 --- a/schema/schema_merge.go +++ b/schema/schema_merge.go @@ -51,7 +51,7 @@ func (m *SchemaMerger) SchemaForModule(meta *module.Meta) (*schema.BodySchema, e return nil, coreSchemaRequiredErr{} } - if meta == nil || m.schemaReader == nil { + if meta == nil { return m.coreSchema, nil } @@ -72,95 +72,97 @@ func (m *SchemaMerger) SchemaForModule(meta *module.Meta) (*schema.BodySchema, e providerRefs := ProviderReferences(meta.ProviderReferences) - for pAddr, pVersionCons := range meta.ProviderRequirements { - pSchema, err := m.schemaReader.ProviderSchema(meta.Path, pAddr, pVersionCons) - if err != nil { - continue - } - - refs := providerRefs.ReferencesOfProvider(pAddr) - for _, localRef := range refs { - if pSchema.Provider != nil { - mergedSchema.Blocks["provider"].DependentBody[schema.NewSchemaKey(schema.DependencyKeys{ - Labels: []schema.LabelDependent{ - {Index: 0, Value: localRef.LocalName}, - }, - })] = pSchema.Provider - } - - providerAddr := lang.Address{ - lang.RootStep{Name: localRef.LocalName}, - } - if localRef.Alias != "" { - providerAddr = append(providerAddr, lang.AttrStep{Name: localRef.Alias}) + if m.schemaReader != nil { + for pAddr, pVersionCons := range meta.ProviderRequirements { + pSchema, err := m.schemaReader.ProviderSchema(meta.Path, pAddr, pVersionCons) + if err != nil { + continue } - for rName, rSchema := range pSchema.Resources { - depKeys := schema.DependencyKeys{ - Labels: []schema.LabelDependent{ - {Index: 0, Value: rName}, - }, - Attributes: []schema.AttributeDependent{ - { - Name: "provider", - Expr: schema.ExpressionValue{ - Address: providerAddr, - }, + refs := providerRefs.ReferencesOfProvider(pAddr) + for _, localRef := range refs { + if pSchema.Provider != nil { + mergedSchema.Blocks["provider"].DependentBody[schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: localRef.LocalName}, }, - }, + })] = pSchema.Provider + } + + providerAddr := lang.Address{ + lang.RootStep{Name: localRef.LocalName}, + } + if localRef.Alias != "" { + providerAddr = append(providerAddr, lang.AttrStep{Name: localRef.Alias}) } - mergedSchema.Blocks["resource"].DependentBody[schema.NewSchemaKey(depKeys)] = rSchema - // No explicit association is required - // if the resource prefix matches provider name - if strings.HasPrefix(rName, localRef.LocalName+"_") { + for rName, rSchema := range pSchema.Resources { depKeys := schema.DependencyKeys{ Labels: []schema.LabelDependent{ {Index: 0, Value: rName}, }, + Attributes: []schema.AttributeDependent{ + { + Name: "provider", + Expr: schema.ExpressionValue{ + Address: providerAddr, + }, + }, + }, } mergedSchema.Blocks["resource"].DependentBody[schema.NewSchemaKey(depKeys)] = rSchema - } - } - for dsName, dsSchema := range pSchema.DataSources { - depKeys := schema.DependencyKeys{ - Labels: []schema.LabelDependent{ - {Index: 0, Value: dsName}, - }, - Attributes: []schema.AttributeDependent{ - { - Name: "provider", - Expr: schema.ExpressionValue{ - Address: providerAddr, + // No explicit association is required + // if the resource prefix matches provider name + if strings.HasPrefix(rName, localRef.LocalName+"_") { + depKeys := schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: rName}, }, - }, - }, - } - - // Add backend-related core bits of schema - if isRemoteStateDataSource(pAddr, dsName) { - dsSchema.Attributes["backend"].IsDepKey = true - dsSchema.Attributes["backend"].Expr = backends.BackendTypesAsExprConstraints(m.terraformVersion) - - delete(dsSchema.Attributes, "config") - depBodies := m.dependentBodyForRemoteStateDataSource(providerAddr, localRef) - for key, depBody := range depBodies { - mergedSchema.Blocks["data"].DependentBody[key] = depBody + } + mergedSchema.Blocks["resource"].DependentBody[schema.NewSchemaKey(depKeys)] = rSchema } } - mergedSchema.Blocks["data"].DependentBody[schema.NewSchemaKey(depKeys)] = dsSchema - - // No explicit association is required - // if the resource prefix matches provider name - if strings.HasPrefix(dsName, localRef.LocalName+"_") { + for dsName, dsSchema := range pSchema.DataSources { depKeys := schema.DependencyKeys{ Labels: []schema.LabelDependent{ {Index: 0, Value: dsName}, }, + Attributes: []schema.AttributeDependent{ + { + Name: "provider", + Expr: schema.ExpressionValue{ + Address: providerAddr, + }, + }, + }, } + + // Add backend-related core bits of schema + if isRemoteStateDataSource(pAddr, dsName) { + dsSchema.Attributes["backend"].IsDepKey = true + dsSchema.Attributes["backend"].Expr = backends.BackendTypesAsExprConstraints(m.terraformVersion) + + delete(dsSchema.Attributes, "config") + depBodies := m.dependentBodyForRemoteStateDataSource(providerAddr, localRef) + for key, depBody := range depBodies { + mergedSchema.Blocks["data"].DependentBody[key] = depBody + } + } + mergedSchema.Blocks["data"].DependentBody[schema.NewSchemaKey(depKeys)] = dsSchema + + // No explicit association is required + // if the resource prefix matches provider name + if strings.HasPrefix(dsName, localRef.LocalName+"_") { + depKeys := schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: dsName}, + }, + } + mergedSchema.Blocks["data"].DependentBody[schema.NewSchemaKey(depKeys)] = dsSchema + } } } } @@ -176,17 +178,20 @@ func (m *SchemaMerger) SchemaForModule(meta *module.Meta) (*schema.BodySchema, e } mergedSchema.Blocks["variable"].DependentBody = variableDependentBody(meta.Variables) } + if m.moduleReader != nil { reader := m.moduleReader modules, err := reader.ModuleCalls(meta.Path) if err != nil { return mergedSchema, nil } + for _, module := range modules { modMeta, err := reader.ModuleMeta(module.Path) if err != nil { continue } + depKeys := schema.DependencyKeys{ // Fetching based only on the source can cause conflicts for multiple versions of the same module // specially if they have different versions or the source of those modules have been modified @@ -205,6 +210,31 @@ func (m *SchemaMerger) SchemaForModule(meta *module.Meta) (*schema.BodySchema, e if err == nil { mergedSchema.Blocks["module"].DependentBody[schema.NewSchemaKey(depKeys)] = depSchema } + + // There's likely more edge cases with how source address can be represented in config + // vs in module manifest, but for now we at least account for the common case of TF Registry + if strings.HasPrefix(module.SourceAddr, "registry.terraform.io/") { + shortName := strings.TrimPrefix(module.SourceAddr, "registry.terraform.io/") + + depKeys := schema.DependencyKeys{ + // Fetching based only on the source can cause conflicts for multiple versions of the same module + // specially if they have different versions or the source of those modules have been modified + // inside the .terraform folder. This is a compromise that we made in this moment since it would impact only auto completion + Attributes: []schema.AttributeDependent{ + { + Name: "source", + Expr: schema.ExpressionValue{ + Static: cty.StringVal(shortName), + }, + }, + }, + } + + depSchema, err := schemaForDependentModuleBlock(module.LocalName, modMeta) + if err == nil { + mergedSchema.Blocks["module"].DependentBody[schema.NewSchemaKey(depKeys)] = depSchema + } + } } } return mergedSchema, nil diff --git a/schema/schema_merge_test.go b/schema/schema_merge_test.go index 4674f2a0..52d41206 100644 --- a/schema/schema_merge_test.go +++ b/schema/schema_merge_test.go @@ -279,6 +279,24 @@ func TestMergeWithJsonProviderSchemasAndModuleVariables_v015(t *testing.T) { } } +func TestMergeWithJsonProviderSchemasAndModuleVariables_registryModule(t *testing.T) { + sm := NewSchemaMerger(testCoreSchema()) + sm.SetModuleReader(testRegistryModuleReader()) + sm.SetTerraformVersion(v0_15_0) + meta := testModuleMeta(t, "testdata/test-config-remote-module.tf") + t.Logf("meta: %#v", meta) + mergedSchema, err := sm.SchemaForModule(meta) + if err != nil { + t.Fatal(err) + } + + moduleSchema := mergedSchema.Blocks["module"] + + if diff := cmp.Diff(expectedRemoteModuleSchema, moduleSchema, ctydebug.CmpOptions); diff != "" { + t.Fatalf("schema differs: %s", diff) + } +} + func testModuleMeta(t *testing.T, path string) *module.Meta { b, err := ioutil.ReadFile(path) if err != nil { @@ -368,6 +386,38 @@ func (m *testModuleReaderStruct) ModuleMeta(modPath string) (*module.Meta, error return nil, fmt.Errorf("invalid source") } +func testRegistryModuleReader() ModuleReader { + return &testRegistryModuleReaderStruct{} +} + +type testRegistryModuleReaderStruct struct { +} + +func (m *testRegistryModuleReaderStruct) ModuleCalls(modPath string) ([]module.ModuleCall, error) { + return []module.ModuleCall{ + { + LocalName: "remote-example", + SourceAddr: "registry.terraform.io/namespace/foobar", + Path: ".terraform/modules/remote-example", + }, + }, nil +} + +func (m *testRegistryModuleReaderStruct) ModuleMeta(modPath string) (*module.Meta, error) { + if modPath == ".terraform/modules/remote-example" { + return &module.Meta{ + Path: ".terraform/modules/remote-example", + Variables: map[string]module.Variable{ + "test": { + Type: cty.String, + Description: "test var", + }, + }, + }, nil + } + return nil, fmt.Errorf("invalid source") +} + func (r *testJsonSchemaReader) ProviderSchema(_ string, pAddr tfaddr.Provider, _ version.Constraints) (*ProviderSchema, error) { if newAddr, ok := r.migrations[pAddr]; ok { pAddr = newAddr diff --git a/schema/schema_merge_v015_test.go b/schema/schema_merge_v015_test.go index a0d6028f..f4f1ed38 100644 --- a/schema/schema_merge_v015_test.go +++ b/schema/schema_merge_v015_test.go @@ -563,3 +563,114 @@ var expectedMergedSchemaWithModule_v015 = &schema.BodySchema{ "module": &moduleWithDependency, }, } + +var expectedRemoteModuleSchema = &schema.BlockSchema{ + Labels: []*schema.LabelSchema{ + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "source": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + IsDepKey: true, + }, + "version": { + Expr: schema.LiteralTypeOnly(cty.String), + IsOptional: true, + }, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Attributes: []schema.AttributeDependent{ + { + Name: "source", + Expr: schema.ExpressionValue{ + Static: cty.StringVal("namespace/foobar"), + }, + }, + }}): { + TargetableAs: []*schema.Targetable{ + { + Address: lang.Address{ + lang.RootStep{Name: "module"}, + lang.AttrStep{Name: "remote-example"}, + }, + ScopeId: refscope.ModuleScope, + AsType: cty.Object(map[string]cty.Type{}), + NestedTargetables: []*schema.Targetable{}, + }, + }, + Attributes: map[string]*schema.AttributeSchema{ + "test": { + Description: lang.PlainText("test var"), + Expr: schema.ExprConstraints{ + schema.TraversalExpr{OfType: cty.String}, + schema.LiteralTypeExpr{Type: cty.String}, + }, + IsRequired: true, + OriginForTarget: &schema.PathTarget{ + Address: schema.Address{ + schema.StaticStep{Name: "var"}, + schema.AttrNameStep{}, + }, + Path: lang.Path{ + Path: ".terraform/modules/remote-example", + LanguageID: "terraform", + }, + Constraints: schema.Constraints{ + ScopeId: "variable", + Type: cty.String, + }, + }, + }, + }, + }, + schema.NewSchemaKey(schema.DependencyKeys{ + Attributes: []schema.AttributeDependent{ + { + Name: "source", + Expr: schema.ExpressionValue{ + Static: cty.StringVal("registry.terraform.io/namespace/foobar"), + }, + }, + }}): { + TargetableAs: []*schema.Targetable{ + { + Address: lang.Address{ + lang.RootStep{Name: "module"}, + lang.AttrStep{Name: "remote-example"}, + }, + ScopeId: refscope.ModuleScope, + AsType: cty.Object(map[string]cty.Type{}), + NestedTargetables: []*schema.Targetable{}, + }, + }, + Attributes: map[string]*schema.AttributeSchema{ + "test": { + Description: lang.PlainText("test var"), + Expr: schema.ExprConstraints{ + schema.TraversalExpr{OfType: cty.String}, + schema.LiteralTypeExpr{Type: cty.String}, + }, + IsRequired: true, + OriginForTarget: &schema.PathTarget{ + Address: schema.Address{ + schema.StaticStep{Name: "var"}, + schema.AttrNameStep{}, + }, + Path: lang.Path{ + Path: ".terraform/modules/remote-example", + LanguageID: "terraform", + }, + Constraints: schema.Constraints{ + ScopeId: "variable", + Type: cty.String, + }, + }, + }, + }, + }, + }, +} diff --git a/schema/testdata/test-config-remote-module.tf b/schema/testdata/test-config-remote-module.tf new file mode 100644 index 00000000..4425eecc --- /dev/null +++ b/schema/testdata/test-config-remote-module.tf @@ -0,0 +1,3 @@ +module "remote-example" { + source = "namespace/foobar" +}