Skip to content

Generate config for list results #37173

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

Open
wants to merge 4 commits into
base: dbanck/tfquery-cli
Choose a base branch
from
Open
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
8 changes: 7 additions & 1 deletion internal/command/jsonlist/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ type QueryResult struct {
Resource map[string]json.RawMessage `json:"resource,omitempty"`

// TODO
// Address string `json:"address,omitempty"`
Address string `json:"address,omitempty"`
// Config string `json:"config,omitempty"`

ResourceConfig string `json:"resource_config,omitempty"`
ImportConfig string `json:"import_config,omitempty"`
}

func MarshalForRenderer(
Expand Down Expand Up @@ -79,6 +82,9 @@ func marshalQueryInstance(rc *plans.QueryInstanceSrc, schemas *terraform.Schemas
Identity: marshalValues(value.GetAttr("identity")),
Resource: marshalValues(value.GetAttr("state")),
}
config := query.Results.Generated.Results[result.Address]
result.ResourceConfig = string(config.Body)
result.ImportConfig = string(config.Import)

ret.Results = append(ret.Results, result)
}
Expand Down
168 changes: 150 additions & 18 deletions internal/genconfig/generate_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package genconfig

import (
"bytes"
"encoding/json"
"fmt"
"maps"
Expand All @@ -21,6 +22,51 @@ import (
"github.com/hashicorp/terraform/internal/tfdiags"
)

type Resource struct {
// HCL Body of the resource, which is the attributes and blocks
// that are part of the resource.
Body []byte

// Import is the HCL code for the import block. This is only
// generated for list resource results.
Import []byte
Addr addrs.AbsResourceInstance
Results map[string]*Resource
}

func (r *Resource) String() string {
var buf strings.Builder
switch r.Addr.Resource.Resource.Mode {
case addrs.ListResourceMode:
last := len(r.Results) - 1
// sort the results by their keys so the output is consistent
for idx, key := range slices.Sorted(maps.Keys(r.Results)) {
managed := r.Results[key]
if managed.Body != nil {
buf.WriteString(managed.String())
buf.WriteString("\n")
}
if managed.Import != nil {
buf.WriteString(string(managed.Import))
buf.WriteString("\n")
}
if idx != last {
buf.WriteString("\n")
}
}
case addrs.ManagedResourceMode:
buf.WriteString(fmt.Sprintf("resource %q %q {\n", r.Addr.Resource.Resource.Type, r.Addr.Resource.Resource.Name))
buf.Write(r.Body)
buf.WriteString("}")
default:
panic(fmt.Errorf("unsupported resource mode %s", r.Addr.Resource.Resource.Mode))
}

// The output better be valid HCL which can be parsed and formatted.
formatted := hclwrite.Format([]byte(buf.String()))
return string(formatted)
}

// GenerateResourceContents generates HCL configuration code for the provided
// resource and state value.
//
Expand All @@ -30,7 +76,7 @@ import (
func GenerateResourceContents(addr addrs.AbsResourceInstance,
schema *configschema.Block,
pc addrs.LocalProviderConfig,
stateVal cty.Value) (string, tfdiags.Diagnostics) {
stateVal cty.Value) (*Resource, tfdiags.Diagnostics) {
var buf strings.Builder

var diags tfdiags.Diagnostics
Expand All @@ -44,25 +90,101 @@ func GenerateResourceContents(addr addrs.AbsResourceInstance,
diags = diags.Append(writeConfigAttributes(addr, &buf, schema.Attributes, 2))
diags = diags.Append(writeConfigBlocks(addr, &buf, schema.BlockTypes, 2))
} else {
diags = diags.Append(writeConfigAttributesFromExisting(addr, &buf, stateVal, schema.Attributes, 2))
diags = diags.Append(writeConfigAttributesFromExisting(addr, &buf, stateVal, schema.Attributes, 2, optionalOrRequiredProcessor))
diags = diags.Append(writeConfigBlocksFromExisting(addr, &buf, stateVal, schema.BlockTypes, 2))
}

// The output better be valid HCL which can be parsed and formatted.
formatted := hclwrite.Format([]byte(buf.String()))
return string(formatted), diags
return &Resource{
Body: formatted,
Addr: addr,
}, diags
}

func WrapResourceContents(addr addrs.AbsResourceInstance, config string) string {
func GenerateListResourceContents(addr addrs.AbsResourceInstance,
schema *configschema.Block,
idSchema *configschema.Object,
pc addrs.LocalProviderConfig,
stateVal cty.Value,
) (*Resource, tfdiags.Diagnostics) {
hclFmt := func(s []byte) []byte {
return bytes.TrimSpace(hclwrite.Format(s))
}
ret := make(map[string]*Resource)
var diags tfdiags.Diagnostics
if !stateVal.CanIterateElements() {
diags = diags.Append(
hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid resource instance value",
Detail: fmt.Sprintf("Resource instance %s has nil or non-iterable value", addr),
})
return nil, diags
}

iter := stateVal.ElementIterator()
for idx := 0; iter.Next(); idx++ {
// Generate a unique resource name for each instance in the list.
resAddr := addrs.AbsResourceInstance{
Module: addr.Module,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: addr.Resource.Resource.Type,
Name: fmt.Sprintf("%s_%d", addr.Resource.Resource.Name, idx),
},
Key: addr.Resource.Key,
},
}
ls := &Resource{Addr: resAddr}

_, val := iter.Element()
// we still need to generate the resource block even if the state is not given,
// so that the import block can reference it.
stateVal := cty.NilVal
if val.Type().HasAttribute("state") {
stateVal = val.GetAttr("state")
}
content, gDiags := GenerateResourceContents(resAddr, schema, pc, stateVal)
if gDiags.HasErrors() {
diags = diags.Append(gDiags)
continue
}
ls.Body = content.Body

idVal := val.GetAttr("identity")
importContent, gDiags := generateImportBlock(resAddr, idSchema, pc, idVal)
if gDiags.HasErrors() {
diags = diags.Append(gDiags)
continue
}
ls.Import = hclFmt([]byte(importContent))

ret[resAddr.String()] = ls
}

return &Resource{
Results: ret,
Addr: addr,
}, diags
}

func generateImportBlock(addr addrs.AbsResourceInstance, idSchema *configschema.Object, pc addrs.LocalProviderConfig, identity cty.Value) (string, tfdiags.Diagnostics) {
var buf strings.Builder
var diags tfdiags.Diagnostics

buf.WriteString(fmt.Sprintf("resource %q %q {\n", addr.Resource.Resource.Type, addr.Resource.Resource.Name))
buf.WriteString(config)
buf.WriteString("}")
buf.WriteString("\n")
buf.WriteString("import {\n")
buf.WriteString(fmt.Sprintf(" to = %s\n", addr.String()))
buf.WriteString(fmt.Sprintf(" provider = %s\n", pc.StringCompact()))
buf.WriteString(" identity = {\n")
diags = diags.Append(writeConfigAttributesFromExisting(addr, &buf, identity, idSchema.Attributes, 2, allowAllAttributesProcessor))
buf.WriteString(strings.Repeat(" ", 2))
buf.WriteString("}\n}\n")

// The output better be valid HCL which can be parsed and formatted.
formatted := hclwrite.Format([]byte(buf.String()))
return string(formatted)
return string(formatted), diags
}

func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder, attrs map[string]*configschema.Attribute, indent int) tfdiags.Diagnostics {
Expand Down Expand Up @@ -112,7 +234,16 @@ func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder,
return diags
}

func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, stateVal cty.Value, attrs map[string]*configschema.Attribute, indent int) tfdiags.Diagnostics {
func optionalOrRequiredProcessor(attr *configschema.Attribute) bool {
// Exclude computed-only attributes
return attr.Optional || attr.Required
}

func allowAllAttributesProcessor(attr *configschema.Attribute) bool {
return true
}

func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, stateVal cty.Value, attrs map[string]*configschema.Attribute, indent int, processAttr func(*configschema.Attribute) bool) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if len(attrs) == 0 {
return diags
Expand All @@ -126,8 +257,7 @@ func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *stri
continue
}

// Exclude computed-only attributes
if attrS.Required || attrS.Optional {
if processAttr != nil && processAttr(attrS) {
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s = ", name))

Expand Down Expand Up @@ -327,6 +457,7 @@ func writeConfigBlocksFromExisting(addr addrs.AbsResourceInstance, buf *strings.

func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.Attribute, stateVal cty.Value, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
processor := optionalOrRequiredProcessor

switch schema.NestedType.Nesting {
case configschema.NestingSingle:
Expand Down Expand Up @@ -354,7 +485,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,

buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s = {\n", name))
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, nestedVal, schema.NestedType.Attributes, indent+2))
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, nestedVal, schema.NestedType.Attributes, indent+2, processor))
buf.WriteString("}\n")
return diags

Expand Down Expand Up @@ -386,7 +517,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
}

buf.WriteString("{\n")
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.NestedType.Attributes, indent+4))
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.NestedType.Attributes, indent+4, processor))
buf.WriteString(strings.Repeat(" ", indent+2))
buf.WriteString("},\n")
}
Expand Down Expand Up @@ -424,7 +555,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
}

buf.WriteString("\n")
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, vals[key], schema.NestedType.Attributes, indent+4))
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, vals[key], schema.NestedType.Attributes, indent+4, processor))
buf.WriteString(strings.Repeat(" ", indent+2))
buf.WriteString("}\n")
}
Expand All @@ -440,6 +571,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,

func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.NestedBlock, stateVal cty.Value, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
processAttr := optionalOrRequiredProcessor

switch schema.Nesting {
case configschema.NestingSingle, configschema.NestingGroup:
Expand All @@ -455,7 +587,7 @@ func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *str
return diags
}
buf.WriteString("\n")
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, stateVal, schema.Attributes, indent+2))
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, stateVal, schema.Attributes, indent+2, processAttr))
diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, stateVal, schema.BlockTypes, indent+2))
buf.WriteString("}\n")
return diags
Expand All @@ -469,7 +601,7 @@ func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *str
for i := range listVals {
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s {\n", name))
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.Attributes, indent+2))
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.Attributes, indent+2, processAttr))
diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, listVals[i], schema.BlockTypes, indent+2))
buf.WriteString("}\n")
}
Expand All @@ -491,7 +623,7 @@ func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *str
return diags
}
buf.WriteString("\n")
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, vals[key], schema.Attributes, indent+2))
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, vals[key], schema.Attributes, indent+2, processAttr))
diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, vals[key], schema.BlockTypes, indent+2))
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString("}\n")
Expand Down
Loading