Generate config for list results (#37173)

pull/37294/head
Samsondeen 10 months ago committed by GitHub
parent 79187d579d
commit 8d8b2bb694
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -242,6 +242,21 @@ func maybeWriteGeneratedConfig(plan *plans.Plan, out string) (wroteConfig bool,
return false, diags.Append(moreDiags)
}
}
// When running a list operation, the results are stored as queries and the
// resource changes above are not populated.
for _, q := range plan.Changes.Queries {
change := genconfig.Change{
Addr: q.Addr.String(),
GeneratedConfig: q.Generated.String(),
}
var moreDiags tfdiags.Diagnostics
writer, _, moreDiags = change.MaybeWriteConfig(writer, out)
if moreDiags.HasErrors() {
return false, diags.Append(moreDiags)
}
}
}
if wroteConfig {

@ -247,10 +247,15 @@ func (h *jsonHook) PreListQuery(id terraform.HookResourceIdentity, input_config
func (h *jsonHook) PostListQuery(id terraform.HookResourceIdentity, results plans.QueryResults) (terraform.HookAction, error) {
addr := id.Addr
data := results.Value.GetAttr("data")
for it := data.ElementIterator(); it.Next(); {
_, value := it.Element()
iter := data.ElementIterator()
for idx := 0; iter.Next(); idx++ {
_, value := iter.Element()
result := json.NewQueryResult(addr, value)
generated := results.Generated
if generated != nil {
generated = generated.Results[idx]
}
result := json.NewQueryResult(addr, value, generated)
h.view.log.Info(
fmt.Sprintf("%s: Result found", addr.String()),

@ -7,6 +7,7 @@ import (
"encoding/json"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/genconfig"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
)
@ -24,6 +25,7 @@ type QueryResult struct {
ResourceType string `json:"resource_type"`
ResourceObject map[string]json.RawMessage `json:"resource_object,omitempty"`
Config string `json:"config,omitempty"`
ImportConfig string `json:"import_config,omitempty"`
}
func NewQueryStart(addr addrs.AbsResourceInstance, input_config cty.Value) QueryStart {
@ -34,15 +36,22 @@ func NewQueryStart(addr addrs.AbsResourceInstance, input_config cty.Value) Query
}
}
func NewQueryResult(addr addrs.AbsResourceInstance, value cty.Value) QueryResult {
return QueryResult{
Address: addr.String(),
func NewQueryResult(listAddr addrs.AbsResourceInstance, value cty.Value, generated *genconfig.Resource) QueryResult {
var config, importConfig string
if generated != nil {
config = generated.String()
importConfig = string(generated.Import)
}
result := QueryResult{
Address: listAddr.String(),
DisplayName: value.GetAttr("display_name").AsString(),
Identity: marshalValues(value.GetAttr("identity")),
ResourceType: addr.Resource.Resource.Type,
ResourceType: listAddr.Resource.Resource.Type,
ResourceObject: marshalValues(value.GetAttr("state")),
// TODO: Add config once we have it available
Config: config,
ImportConfig: importConfig,
}
return result
}
func marshalValues(value cty.Value) map[string]json.RawMessage {

@ -4,6 +4,7 @@
package genconfig
import (
"bytes"
"encoding/json"
"fmt"
"maps"
@ -21,6 +22,50 @@ 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 []*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, managed := range r.Results {
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.
//
@ -30,7 +75,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
@ -44,25 +89,97 @@ 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) {
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
}
ret := make([]*Resource, stateVal.LengthInt())
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}
ret[idx] = ls
_, 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 = bytes.TrimSpace(hclwrite.Format([]byte(importContent)))
}
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 {
@ -112,7 +229,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
@ -126,8 +252,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))
@ -327,6 +452,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:
@ -354,7 +480,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
@ -386,7 +512,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")
}
@ -424,7 +550,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")
}
@ -440,6 +566,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:
@ -455,7 +582,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
@ -469,7 +596,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")
}
@ -491,7 +618,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")

@ -830,7 +830,7 @@ resource "tfcoremock_sensitive_values" "values" {
t.Errorf("expected no diagnostics but found %s", diags)
}
got := WrapResourceContents(tc.addr, contents)
got := contents.String()
want := strings.TrimSpace(tc.expected)
if diff := cmp.Diff(got, want); len(diff) > 0 {
t.Errorf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
@ -846,3 +846,179 @@ func sensitiveAttribute(t cty.Type) *configschema.Attribute {
Sensitive: true,
}
}
func TestGenerateResourceAndIDContents(t *testing.T) {
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {
Type: cty.String,
Optional: true,
},
"id": {
Type: cty.String,
Computed: true,
},
"tags": {
Type: cty.Map(cty.String),
Optional: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"network_interface": {
Nesting: configschema.NestingList,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"subnet_id": {
Type: cty.String,
Required: true,
},
"ip_address": {
Type: cty.String,
Optional: true,
},
},
},
},
},
}
// Define the identity schema
idSchema := &configschema.Object{
Nesting: configschema.NestingSingle,
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Optional: true,
},
},
}
// Create mock resource instance values
value := cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"state": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("instance-1"),
"id": cty.StringVal("i-abcdef"),
"tags": cty.MapVal(map[string]cty.Value{
"Environment": cty.StringVal("Dev"),
"Owner": cty.StringVal("Team1"),
}),
"network_interface": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"subnet_id": cty.StringVal("subnet-123"),
"ip_address": cty.StringVal("10.0.0.1"),
}),
}),
}),
"identity": cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-abcdef"),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"state": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("instance-2"),
"id": cty.StringVal("i-123456"),
"tags": cty.MapVal(map[string]cty.Value{
"Environment": cty.StringVal("Prod"),
"Owner": cty.StringVal("Team2"),
}),
"network_interface": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"subnet_id": cty.StringVal("subnet-456"),
"ip_address": cty.StringVal("10.0.0.2"),
}),
}),
}),
"identity": cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-123456"),
}),
}),
})
// Create test resource address
addr := addrs.AbsResource{
Module: addrs.RootModuleInstance,
Resource: addrs.Resource{
Mode: addrs.ListResourceMode,
Type: "aws_instance",
Name: "example",
},
}
// Create instance addresses for each instance
instAddr1 := addr.Instance(addrs.NoKey)
// Create provider config
pc := addrs.LocalProviderConfig{
LocalName: "aws",
}
// Generate content
content, diags := GenerateListResourceContents(instAddr1, schema, idSchema, pc, value)
// Check for diagnostics
if diags.HasErrors() {
t.Fatalf("unexpected diagnostics: %s", diags.Err())
}
// Check the generated content
expectedContent := `resource "aws_instance" "example_0" {
name = "instance-1"
tags = {
Environment = "Dev"
Owner = "Team1"
}
network_interface {
ip_address = "10.0.0.1"
subnet_id = "subnet-123"
}
}
import {
to = aws_instance.example_0
provider = aws
identity = {
id = "i-abcdef"
}
}
resource "aws_instance" "example_1" {
name = "instance-2"
tags = {
Environment = "Prod"
Owner = "Team2"
}
network_interface {
ip_address = "10.0.0.2"
subnet_id = "subnet-456"
}
}
import {
to = aws_instance.example_1
provider = aws
identity = {
id = "i-123456"
}
}
`
// Normalize both strings by removing extra whitespace for comparison
normalizeString := func(s string) string {
// Remove spaces at the end of lines and replace multiple newlines with a single one
lines := strings.Split(s, "\n")
for i, line := range lines {
lines[i] = strings.TrimRight(line, " \t")
}
return strings.Join(lines, "\n")
}
normalizedExpected := normalizeString(expectedContent)
var merged string
res := content.Results
for _, addr := range res {
merged += addr.String()
}
normalizedActual := normalizeString(content.String())
if diff := cmp.Diff(normalizedExpected, normalizedActual); diff != "" {
t.Errorf("Generated content doesn't match expected. want:\n%s\ngot:\n%s\ndiff:\n%s", normalizedExpected, normalizedActual, diff)
}
}

@ -69,7 +69,7 @@ func (c *Change) MaybeWriteConfig(writer io.Writer, out string) (io.Writer, bool
header += fmt.Sprintf(" from %q", c.ImportID)
}
header += "\n"
if _, err := writer.Write([]byte(fmt.Sprintf("%s%s\n", header, c.GeneratedConfig))); err != nil {
if _, err := writer.Write(fmt.Appendf(nil, "%s%s\n", header, c.GeneratedConfig)); err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Failed to save generated config",

@ -9,6 +9,7 @@ import (
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/genconfig"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/schemarepo"
@ -251,7 +252,8 @@ type QueryInstance struct {
}
type QueryResults struct {
Value cty.Value
Value cty.Value
Generated *genconfig.Resource
}
func (qi *QueryInstance) DeepCopy() *QueryInstance {
@ -273,6 +275,7 @@ func (rc *QueryInstance) Encode(schema providers.Schema) (*QueryInstanceSrc, err
Addr: rc.Addr,
Results: results,
ProviderAddr: rc.ProviderAddr,
Generated: rc.Results.Generated,
}, nil
}

@ -9,6 +9,7 @@ import (
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/genconfig"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/schemarepo"
@ -176,11 +177,10 @@ func (c *ChangesSrc) AppendResourceInstanceChange(change *ResourceInstanceChange
}
type QueryInstanceSrc struct {
Addr addrs.AbsResourceInstance
Addr addrs.AbsResourceInstance
ProviderAddr addrs.AbsProviderConfig
Results DynamicValue
Results DynamicValue
Generated *genconfig.Resource
}
func (qis *QueryInstanceSrc) Decode(schema providers.Schema) (*QueryInstance, error) {
@ -192,7 +192,8 @@ func (qis *QueryInstanceSrc) Decode(schema providers.Schema) (*QueryInstance, er
return &QueryInstance{
Addr: qis.Addr,
Results: QueryResults{
Value: query,
Value: query,
Generated: qis.Generated,
},
ProviderAddr: qis.ProviderAddr,
}, nil

@ -5,6 +5,7 @@ package terraform
import (
"fmt"
"maps"
"sort"
"strings"
"testing"
@ -22,10 +23,12 @@ import (
)
func TestContext2Plan_queryList(t *testing.T) {
cases := []struct {
name string
mainConfig string
queryConfig string
generatedPath string
diagCount int
expectedErrMsg []string
assertState func(*states.State)
@ -33,7 +36,7 @@ func TestContext2Plan_queryList(t *testing.T) {
listResourceFn func(request providers.ListResourceRequest) providers.ListResourceResponse
}{
{
name: "valid list reference",
name: "valid list reference - generates config",
mainConfig: `
terraform {
required_providers {
@ -71,6 +74,7 @@ func TestContext2Plan_queryList(t *testing.T) {
}
}
`,
generatedPath: t.TempDir(),
listResourceFn: func(request providers.ListResourceRequest) providers.ListResourceResponse {
madeUp := []cty.Value{
cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-123456")}),
@ -85,68 +89,62 @@ func TestContext2Plan_queryList(t *testing.T) {
}
resp := []cty.Value{}
if request.IncludeResourceObject {
for i, v := range madeUp {
resp = append(resp, cty.ObjectVal(map[string]cty.Value{
"state": v,
"identity": ids[i],
"display_name": cty.StringVal(fmt.Sprintf("Instance %d", i+1)),
}))
for i, v := range madeUp {
mp := map[string]cty.Value{
"identity": ids[i],
"display_name": cty.StringVal(fmt.Sprintf("Instance %d", i+1)),
}
if request.IncludeResourceObject {
mp["state"] = v
}
resp = append(resp, cty.ObjectVal(mp))
}
ret := map[string]cty.Value{
ret := request.Config.AsValueMap()
maps.Copy(ret, map[string]cty.Value{
"data": cty.TupleVal(resp),
}
for k, v := range request.Config.AsValueMap() {
if k != "data" {
ret[k] = v
}
}
})
return providers.ListResourceResponse{Result: cty.ObjectVal(ret)}
},
assertChanges: func(sch providers.ProviderSchema, changes *plans.ChangesSrc) {
expectedResources := map[string][]string{
"list.test_resource.test": {"ami-123456", "ami-654321", "ami-789012"},
"list.test_resource.test2": {},
}
actualResources := map[string][]string{}
expectedResources := []string{"list.test_resource.test", "list.test_resource.test2"}
actualResources := make([]string, 0)
generatedCfgs := make([]string, 0)
for _, change := range changes.Queries {
actualResources = append(actualResources, change.Addr.String())
schema := sch.ListResourceTypes[change.Addr.Resource.Resource.Type]
cs, err := change.Decode(schema)
if err != nil {
t.Fatalf("failed to decode change: %s", err)
}
// Verify instance types
actualTypes := make([]string, 0)
obj := cs.Results.Value.GetAttr("data")
if obj.IsNull() {
t.Fatalf("Expected 'data' attribute to be present, but it is null")
}
obj.ForEachElement(func(key cty.Value, val cty.Value) bool {
if !val.Type().HasAttribute("state") {
t.Fatalf("Expected 'state' attribute to be present, but it is missing")
}
val = val.GetAttr("state")
if !val.IsNull() {
if val.GetAttr("instance_type").IsNull() {
t.Fatalf("Expected 'instance_type' attribute to be present, but it is missing")
if val.Type().HasAttribute("state") {
val = val.GetAttr("state")
if !val.IsNull() {
if val.GetAttr("instance_type").IsNull() {
t.Fatalf("Expected 'instance_type' attribute to be present, but it is missing")
}
}
actualTypes = append(actualTypes, val.GetAttr("instance_type").AsString())
}
return false
})
sort.Strings(actualTypes)
actualResources[change.Addr.String()] = actualTypes
generatedCfgs = append(generatedCfgs, change.Generated.String())
}
if diff := cmp.Diff(expectedResources, actualResources); diff != "" {
t.Fatalf("Expected resources to match, but they differ: %s", diff)
}
if diff := cmp.Diff([]string{testResourceCfg, testResourceCfg2}, generatedCfgs); diff != "" {
t.Fatalf("Expected generated configs to match, but they differ: %s", diff)
}
},
},
{
@ -709,9 +707,10 @@ func TestContext2Plan_queryList(t *testing.T) {
tfdiags.AssertNoDiagnostics(t, diags)
plan, diags := ctx.Plan(mod, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
SetVariables: testInputValuesUnset(mod.Module.Variables),
Query: true,
Mode: plans.NormalMode,
SetVariables: testInputValuesUnset(mod.Module.Variables),
Query: true,
GenerateConfigPath: tc.generatedPath,
})
if len(diags) != tc.diagCount {
t.Fatalf("expected %d diagnostics, got %d \n -diags: %s", tc.diagCount, len(diags), diags)
@ -924,6 +923,7 @@ func getListProviderSchemaResp() *providers.GetProviderSchemaResponse {
"instance_type": {
Type: cty.String,
Computed: true,
Optional: true,
},
},
},
@ -962,3 +962,73 @@ func getListProviderSchemaResp() *providers.GetProviderSchemaResponse {
},
})
}
var (
testResourceCfg = `resource "test_resource" "test_0" {
instance_type = "ami-123456"
}
import {
to = test_resource.test_0
provider = test
identity = {
id = "i-v1"
}
}
resource "test_resource" "test_1" {
instance_type = "ami-654321"
}
import {
to = test_resource.test_1
provider = test
identity = {
id = "i-v2"
}
}
resource "test_resource" "test_2" {
instance_type = "ami-789012"
}
import {
to = test_resource.test_2
provider = test
identity = {
id = "i-v3"
}
}
`
testResourceCfg2 = `resource "test_resource" "test2_0" {
instance_type = null # OPTIONAL string
}
import {
to = test_resource.test2_0
provider = test
identity = {
id = "i-v1"
}
}
resource "test_resource" "test2_1" {
instance_type = null # OPTIONAL string
}
import {
to = test_resource.test2_1
provider = test
identity = {
id = "i-v2"
}
}
resource "test_resource" "test2_2" {
instance_type = null # OPTIONAL string
}
import {
to = test_resource.test2_2
provider = test
identity = {
id = "i-v3"
}
}
`
)

@ -46,7 +46,7 @@ type NodeAbstractResourceInstance struct {
preDestroyRefresh bool
// During import we may generate configuration for a resource, which needs
// During import (or query) we may generate configuration for a resource, which needs
// to be stored in the final change.
generatedConfigHCL string

@ -839,16 +839,15 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.
}
// Generate the HCL string first, then parse the HCL body from it.
// First we generate the contents of the resource block for use within
// the planning node. Then we wrap it in an enclosing resource block to
// pass into the plan for rendering.
generatedHCLAttributes, generatedDiags := n.generateHCLStringAttributes(n.Addr, instanceRefreshState, schema.Body)
generatedResource, generatedDiags := n.generateHCLResourceDef(n.Addr, instanceRefreshState.Value, schema)
diags = diags.Append(generatedDiags)
n.generatedConfigHCL = genconfig.WrapResourceContents(n.Addr, generatedHCLAttributes)
// This wraps the content of the resource block in an enclosing resource block
// to pass into the plan for rendering.
n.generatedConfigHCL = generatedResource.String()
// parse the "file" as HCL to get the hcl.Body
synthHCLFile, hclDiags := hclsyntax.ParseConfig([]byte(generatedHCLAttributes), filepath.Base(n.generateConfigPath), hcl.Pos{Byte: 0, Line: 1, Column: 1})
// parse the "file" body as HCL to get the hcl.Body
synthHCLFile, hclDiags := hclsyntax.ParseConfig(generatedResource.Body, filepath.Base(n.generateConfigPath), hcl.Pos{Byte: 0, Line: 1, Column: 1})
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
return instanceRefreshState, nil, diags
@ -883,10 +882,11 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.
return instanceRefreshState, deferred, diags
}
// generateHCLStringAttributes produces a string in HCL format for the given
// resource state and schema without the surrounding block.
func (n *NodePlannableResourceInstance) generateHCLStringAttributes(addr addrs.AbsResourceInstance, state *states.ResourceInstanceObject, schema *configschema.Block) (string, tfdiags.Diagnostics) {
filteredSchema := schema.Filter(
// generateHCLResourceDef generates the HCL definition for the resource
// instance, including the surrounding block. This is used to generate the
// configuration for the resource instance when importing or generating
func (n *NodePlannableResourceInstance) generateHCLResourceDef(addr addrs.AbsResourceInstance, state cty.Value, schema providers.Schema) (*genconfig.Resource, tfdiags.Diagnostics) {
filteredSchema := schema.Body.Filter(
configschema.FilterOr(
configschema.FilterReadOnlyAttribute,
configschema.FilterDeprecatedAttribute,
@ -911,7 +911,15 @@ func (n *NodePlannableResourceInstance) generateHCLStringAttributes(addr addrs.A
Alias: n.ResolvedProvider.Alias,
}
return genconfig.GenerateResourceContents(addr, filteredSchema, providerAddr, state.Value)
switch addr.Resource.Resource.Mode {
case addrs.ManagedResourceMode:
return genconfig.GenerateResourceContents(addr, filteredSchema, providerAddr, state)
case addrs.ListResourceMode:
identitySchema := schema.Identity
return genconfig.GenerateListResourceContents(addr, filteredSchema, identitySchema, providerAddr, state)
default:
panic(fmt.Sprintf("unexpected resource mode %s for resource %s", addr.Resource.Resource.Mode, addr))
}
}
// mergeDeps returns the union of 2 sets of dependencies

@ -95,6 +95,17 @@ func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (di
results := plans.QueryResults{
Value: resp.Result,
}
// If a path is specified, generate the config for the resource
if n.generateConfigPath != "" {
var gDiags tfdiags.Diagnostics
results.Generated, gDiags = n.generateHCLResourceDef(addr, resp.Result.GetAttr("data"), providerSchema.ResourceTypes[n.Config.Type])
diags = diags.Append(gDiags)
if diags.HasErrors() {
return diags
}
}
ctx.Hook(func(h Hook) (HookAction, error) {
return h.PostListQuery(rId, results)
})

@ -181,6 +181,10 @@ func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config) er
importTargets: imports,
}
if r.List != nil {
abstract.generateConfigPath = t.generateConfigPathForImportTargets
}
var node dag.Vertex = abstract
if f := t.Concrete; f != nil {
node = f(abstract)

Loading…
Cancel
Save