Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

decoder: Ensure partially unknown dependent body is handled #339

Merged
merged 2 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions decoder/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@ func (d *PathDecoder) hoverContentForLabel(i int, block *hclsyntax.Block, bSchem
labelSchema := bSchema.Labels[i]

if labelSchema.IsDepKey {
bs, _, ok := schemahelper.NewBlockSchema(bSchema).DependentBodySchema(block.AsHCLBlock())
if ok {
bs, _, result := schemahelper.NewBlockSchema(bSchema).DependentBodySchema(block.AsHCLBlock())
if result == schemahelper.LookupSuccessful || result == schemahelper.LookupPartiallySuccessful {
content := fmt.Sprintf("`%s`", value)
if bs.Detail != "" {
content += " " + bs.Detail
Expand Down
15 changes: 5 additions & 10 deletions decoder/internal/schemahelper/block_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/hashicorp/hcl/v2"
)

func MergeBlockBodySchemas(block *hcl.Block, blockSchema *schema.BlockSchema) (*schema.BodySchema, bool) {
func MergeBlockBodySchemas(block *hcl.Block, blockSchema *schema.BlockSchema) (*schema.BodySchema, LookupResult) {
mergedSchema := &schema.BodySchema{}
if blockSchema.Body != nil {
mergedSchema = blockSchema.Body.Copy()
Expand All @@ -26,8 +26,8 @@ func MergeBlockBodySchemas(block *hcl.Block, blockSchema *schema.BlockSchema) (*
mergedSchema.ImpliedOrigins = make([]schema.ImpliedOrigin, 0)
}

depSchema, depKeys, ok := NewBlockSchema(blockSchema).DependentBodySchema(block)
if ok {
depSchema, _, result := NewBlockSchema(blockSchema).DependentBodySchema(block)
if result == LookupSuccessful || result == LookupPartiallySuccessful {
for name, attr := range depSchema.Attributes {
if _, exists := mergedSchema.Attributes[name]; !exists {
mergedSchema.Attributes[name] = attr
Expand Down Expand Up @@ -71,7 +71,7 @@ func MergeBlockBodySchemas(block *hcl.Block, blockSchema *schema.BlockSchema) (*
if depSchema.Extensions != nil {
mergedSchema.Extensions = depSchema.Extensions.Copy()
}
} else if !ok && mergedSchema.Extensions != nil && mergedSchema.Extensions.DynamicBlocks && len(mergedSchema.Blocks) > 0 {
} else if (result == LookupFailed || result == NoDependentKeys) && mergedSchema.Extensions != nil && mergedSchema.Extensions.DynamicBlocks && len(mergedSchema.Blocks) > 0 {
// dynamic blocks are only relevant for dependent schemas,
// but we may end up here because the schema is a result
// of merged static + dependent schema from previous iteration
Expand All @@ -90,10 +90,5 @@ func MergeBlockBodySchemas(block *hcl.Block, blockSchema *schema.BlockSchema) (*
mergedSchema.Blocks["dynamic"] = buildDynamicBlockSchema(mergedSchema)
}

expectedDepBody := len(depKeys.Labels) > 0 || len(depKeys.Attributes) > 0

// report success either if there wasn't any dependent body merging to do
// or if the merging was successful

return mergedSchema, !expectedDepBody || ok
return mergedSchema, result
}
30 changes: 25 additions & 5 deletions decoder/internal/schemahelper/dependent_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ import (
"github.com/zclconf/go-cty/cty"
)

type LookupResult int

const (
LookupFailed LookupResult = iota
LookupSuccessful
LookupPartiallySuccessful
NoDependentKeys
)

type blockSchema struct {
*schema.BlockSchema
seenNestedDepKeys bool
Expand All @@ -23,16 +32,23 @@ func NewBlockSchema(bs *schema.BlockSchema) blockSchema {

// DependentBodySchema finds relevant BodySchema based on dependency keys
// such as a label or an attribute (or combination of both).
func (bs blockSchema) DependentBodySchema(block *hcl.Block) (*schema.BodySchema, schema.DependencyKeys, bool) {
func (bs blockSchema) DependentBodySchema(block *hcl.Block) (*schema.BodySchema, schema.DependencyKeys, LookupResult) {
result := LookupFailed

dks := dependencyKeysFromBlock(block, bs)
b, err := dks.MarshalJSON()
if err != nil {
return nil, schema.DependencyKeys{}, false
return nil, schema.DependencyKeys{}, result
}

if len(dks.Labels) == 0 && len(dks.Attributes) == 0 {
return bs.Body, schema.DependencyKeys{}, NoDependentKeys
}

key := schema.SchemaKey(string(b))
depBodySchema, ok := bs.DependentBody[key]
if ok {
result = LookupSuccessful
hasDepKeys := false
for _, attr := range depBodySchema.Attributes {
if attr.IsDepKey {
Expand All @@ -44,13 +60,17 @@ func (bs blockSchema) DependentBodySchema(block *hcl.Block) (*schema.BodySchema,
mergedBlockSchema := NewBlockSchema(bs.Copy())
mergedBlockSchema.seenNestedDepKeys = true
mergedBlockSchema.Body = depBodySchema
if depBodySchema, dks, ok := mergedBlockSchema.DependentBodySchema(block); ok {
return depBodySchema, dks, ok
if depBodySchema, dks, nestedOk := mergedBlockSchema.DependentBodySchema(block); nestedOk == LookupSuccessful {
return depBodySchema, dks, LookupSuccessful
} else {
// Ensure we report lookup failure overall if we couldn't
// lookup nested dependent body
result = LookupPartiallySuccessful
}
}
}

return depBodySchema, dks, ok
return depBodySchema, dks, result
}

func dependencyKeysFromBlock(block *hcl.Block, blockSchema blockSchema) schema.DependencyKeys {
Expand Down
111 changes: 92 additions & 19 deletions decoder/internal/schemahelper/dependent_body_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ func TestBodySchema_DependentBodySchema_label_basic(t *testing.T) {
},
}

bodySchema, _, ok := NewBlockSchema(bSchema).DependentBodySchema(block)
if !ok {
bodySchema, _, result := NewBlockSchema(bSchema).DependentBodySchema(block)
if result != LookupSuccessful {
t.Fatal("expected to find body schema for 'theircloud' label")
}
expectedSchema := &schema.BodySchema{
Expand Down Expand Up @@ -104,8 +104,8 @@ func TestBodySchema_DependentBodySchema_mismatchingLabels(t *testing.T) {
},
}

_, _, ok := NewBlockSchema(bSchema).DependentBodySchema(block)
if ok {
_, _, result := NewBlockSchema(bSchema).DependentBodySchema(block)
if result != LookupFailed {
t.Fatal("expected to not find body schema for mismatching label schema")
}
}
Expand Down Expand Up @@ -170,17 +170,17 @@ func TestBodySchema_DependentBodySchema_dependentAttr(t *testing.T) {
},
}

bodySchema, _, ok := NewBlockSchema(bSchema).DependentBodySchema(&hcl.Block{
bodySchema, _, result := NewBlockSchema(bSchema).DependentBodySchema(&hcl.Block{
Labels: []string{"remote_state"},
})
if !ok {
if result != LookupSuccessful {
t.Fatal("expected to find body schema for nested dependent schema")
}
if diff := cmp.Diff(firstDepBody, bodySchema, ctydebug.CmpOptions); diff != "" {
t.Fatalf("mismatching body schema: %s", diff)
}

bodySchema, _, ok = NewBlockSchema(bSchema).DependentBodySchema(&hcl.Block{
bodySchema, _, result = NewBlockSchema(bSchema).DependentBodySchema(&hcl.Block{
Labels: []string{"remote_state"},
Body: &hclsyntax.Body{
Attributes: hclsyntax.Attributes{
Expand All @@ -193,7 +193,7 @@ func TestBodySchema_DependentBodySchema_dependentAttr(t *testing.T) {
},
},
})
if !ok {
if result != LookupSuccessful {
t.Fatal("expected to find body schema for nested dependent schema")
}
if diff := cmp.Diff(secondDepBody, bodySchema, ctydebug.CmpOptions); diff != "" {
Expand Down Expand Up @@ -256,7 +256,7 @@ func TestBodySchema_DependentBodySchema_missingDependentAttr(t *testing.T) {
},
}

bodySchema, _, ok := NewBlockSchema(bSchema).DependentBodySchema(&hcl.Block{
bodySchema, _, result := NewBlockSchema(bSchema).DependentBodySchema(&hcl.Block{
Labels: []string{"remote_state"},
Body: &hclsyntax.Body{
Attributes: hclsyntax.Attributes{
Expand All @@ -267,8 +267,8 @@ func TestBodySchema_DependentBodySchema_missingDependentAttr(t *testing.T) {
},
},
})
if !ok {
t.Fatal("expected to find first body schema for missing keys")
if result != LookupPartiallySuccessful {
t.Fatalf("expected to find first body schema for missing keys; reported: %q", result)
}
if diff := cmp.Diff(firstDepBody, bodySchema, ctydebug.CmpOptions); diff != "" {
t.Fatalf("mismatching body schema: %s", diff)
Expand Down Expand Up @@ -418,8 +418,8 @@ func TestBodySchema_DependentBodySchema_attributes(t *testing.T) {
Attributes: tc.attributes,
},
}
bodySchema, _, ok := tc.schema.DependentBodySchema(block)
if !ok {
bodySchema, _, result := tc.schema.DependentBodySchema(block)
if result != LookupSuccessful {
t.Fatalf("expected to find body schema for given block with %d attributes",
len(tc.attributes))
}
Expand All @@ -430,13 +430,86 @@ func TestBodySchema_DependentBodySchema_attributes(t *testing.T) {
}
}

func TestBodySchema_DependentBodySchema_partialMergeFailure(t *testing.T) {
testSchema := NewBlockSchema(&schema.BlockSchema{
Labels: []*schema.LabelSchema{
{
Name: "type",
IsDepKey: true,
},
},
Body: &schema.BodySchema{
Attributes: map[string]*schema.AttributeSchema{
"count": {
Constraint: schema.AnyExpression{OfType: cty.Number},
},
},
},
DependentBody: map[schema.SchemaKey]*schema.BodySchema{
schema.NewSchemaKey(schema.DependencyKeys{
Labels: []schema.LabelDependent{
{
Index: 0,
Value: "terraform_remote_state",
},
},
}): {
Attributes: map[string]*schema.AttributeSchema{
"first": {
Constraint: schema.LiteralType{Type: cty.String},
},
"backend": {
Constraint: schema.AnyExpression{OfType: cty.String},
IsDepKey: true,
},
},
},
schema.NewSchemaKey(schema.DependencyKeys{
Attributes: []schema.AttributeDependent{
{
Name: "backend",
Expr: schema.ExpressionValue{
Static: cty.StringVal("remote"),
},
},
},
}): {
Attributes: map[string]*schema.AttributeSchema{
"second": {Constraint: schema.LiteralType{Type: cty.String}},
},
},
},
})

block := &hcl.Block{
Labels: []string{"terraform_remote_state"},
Body: &hclsyntax.Body{
Attributes: map[string]*hclsyntax.Attribute{
"backend": {
Name: "backend",
Expr: &hclsyntax.ScopeTraversalExpr{
Traversal: hcl.Traversal{
hcl.TraverseRoot{Name: "referencestep"},
},
},
},
},
},
}

_, result := MergeBlockBodySchemas(block, testSchema.BlockSchema)
if result != LookupPartiallySuccessful {
t.Fatal("expected partially failed dependent body lookup to fail")
}
}

func TestBodySchema_DependentBodySchema_label_notFound(t *testing.T) {
block := &hcl.Block{
Labels: []string{"test", "mycloud"},
Body: hcl.EmptyBody(),
}
_, _, ok := testSchemaWithLabels.DependentBodySchema(block)
if ok {
_, _, result := testSchemaWithLabels.DependentBodySchema(block)
if result != LookupFailed {
t.Fatal("expected not to find body schema for 'mycloud' 2nd label")
}
}
Expand All @@ -446,8 +519,8 @@ func TestBodySchema_DependentBodySchema_label_storedUnsorted(t *testing.T) {
Labels: []string{"complexcloud", "pumpkin"},
Body: hcl.EmptyBody(),
}
bodySchema, _, ok := testSchemaWithLabels.DependentBodySchema(block)
if !ok {
bodySchema, _, result := testSchemaWithLabels.DependentBodySchema(block)
if result != LookupSuccessful {
t.Fatal("expected to find body schema stored with unsorted keys")
}
expectedSchema := &schema.BodySchema{
Expand All @@ -465,8 +538,8 @@ func TestBodySchema_DependentBodySchema_label_lookupUnsorted(t *testing.T) {
Labels: []string{"apple", "crazycloud"},
Body: hcl.EmptyBody(),
}
_, _, ok := testSchemaWithLabels.DependentBodySchema(block)
if ok {
_, _, result := testSchemaWithLabels.DependentBodySchema(block)
if result != LookupFailed {
t.Fatal("expected to not find body schema based on wrongly sorted labels")
}
}
Expand Down
4 changes: 2 additions & 2 deletions decoder/internal/walker/walker.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ func Walk(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema, w
var blockBodySchema schema.Schema = nil
bSchema, ok := nodeSchema.(*schema.BlockSchema)
if ok && bSchema.Body != nil {
mergedSchema, ok := schemahelper.MergeBlockBodySchemas(nodeType.AsHCLBlock(), bSchema)
if !ok {
mergedSchema, result := schemahelper.MergeBlockBodySchemas(nodeType.AsHCLBlock(), bSchema)
if result == schemahelper.LookupFailed || result == schemahelper.LookupPartiallySuccessful {
ctx = schemacontext.WithUnknownSchema(ctx)
}
blockBodySchema = mergedSchema
Expand Down
4 changes: 2 additions & 2 deletions decoder/links.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ func (d *PathDecoder) linksInBody(body *hclsyntax.Body, bodySchema *schema.BodyS

// Currently only block bodies have links associated
if block.Body != nil {
depSchema, dk, ok := schemahelper.NewBlockSchema(blockSchema).DependentBodySchema(block.AsHCLBlock())
if ok && depSchema.DocsLink != nil {
depSchema, dk, result := schemahelper.NewBlockSchema(blockSchema).DependentBodySchema(block.AsHCLBlock())
if (result == schemahelper.LookupSuccessful || result == schemahelper.LookupPartiallySuccessful || result == schemahelper.NoDependentKeys) && depSchema.DocsLink != nil {
link := depSchema.DocsLink
u, err := d.docsURL(link.URL, "documentLink")
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions decoder/reference_targets.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,8 @@ func (d *PathDecoder) decodeReferenceTargetsForBody(body hcl.Body, parentBlock *
}
}

depSchema, _, ok := schemahelper.NewBlockSchema(bSchema).DependentBodySchema(blk.Block)
if ok {
depSchema, _, result := schemahelper.NewBlockSchema(bSchema).DependentBodySchema(blk.Block)
if result == schemahelper.LookupSuccessful {
fullSchema := depSchema
if bSchema.Address.BodyAsData {
mergedSchema, _ := schemahelper.MergeBlockBodySchemas(blk.Block, bSchema)
Expand Down