Skip to content

Commit

Permalink
schema: Account for short (Registry) module source address (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko authored Feb 24, 2022
1 parent 30b46a8 commit b1b7f37
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 68 deletions.
166 changes: 98 additions & 68 deletions schema/schema_merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
}
}
}
}
Expand All @@ -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
Expand All @@ -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
Expand Down
50 changes: 50 additions & 0 deletions schema/schema_merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit b1b7f37

Please sign in to comment.