plannable import: safer config generation and schema filters (#33232)

* genconfig: fix nil nested block panic

* genconfig: null NestingSingle blocks should be absent

A NestingSingle block that is null in state should be completely absent from config.

* configschema: make FilterOr variadic

* configschema: apply filters to nested types

* configschema: filter helper/schema id attribute

The legacy SDK adds an Optional+Computed "id" attribute to the
resource schema even if not defined in provider code.
During validation, however, the presence of an extraneous "id"
attribute in config will cause an error.
Remove this attribute so we do not generate an "id" attribute
where there is a risk that it is not in the real resource schema.

* configschema: filter test

* terraform: do not pre-validate generated config

Config generated from a resource's import state may fail validation in
the case of schema behaviours such as ExactlyOneOf and ConflictsWith.
We don't want to fail the plan now, because that would give the user no
way to proceed and fix the config to make it valid. We allow the plan to
complete and output the generated config.

* generate config alongside import process

Rather than waiting until we call `plan()`, generate the configuration
at the point of the import call, so we have the necessary data to return
in case planning fails later.

The `plan` and `state` predeclared variables in the plan() method were
obfuscating the actual return of nil throughout, so those identifiers
were removed for clarity.

* move generateHCLStringAttributes closer to caller

* store generated config in plan on error

* test for config gen with error

* add simple warning when generating config

---------

Co-authored-by: James Bardin <j.bardin@gmail.com>
This commit is contained in:
kmoe 2023-05-24 11:16:05 +01:00 committed by GitHub
parent f6737d47e7
commit be2ad69eda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 522 additions and 95 deletions

View File

@ -4,5 +4,4 @@
# __generated__ by Terraform from "bar"
resource "test_instance" "foo" {
ami = null
id = "bar"
}

View File

@ -3,10 +3,17 @@ package configschema
type FilterT[T any] func(string, T) bool
var (
FilterReadOnlyAttributes = func(name string, attribute *Attribute) bool {
FilterReadOnlyAttribute = func(name string, attribute *Attribute) bool {
return attribute.Computed && !attribute.Optional
}
FilterHelperSchemaIdAttribute = func(name string, attribute *Attribute) bool {
if name == "id" && attribute.Computed && attribute.Optional {
return true
}
return false
}
FilterDeprecatedAttribute = func(name string, attribute *Attribute) bool {
return attribute.Deprecated
}
@ -16,9 +23,14 @@ var (
}
)
func FilterOr[T any](one, two FilterT[T]) FilterT[T] {
func FilterOr[T any](filters ...FilterT[T]) FilterT[T] {
return func(name string, value T) bool {
return one(name, value) || two(name, value)
for _, f := range filters {
if f(name, value) {
return true
}
}
return false
}
}
@ -36,6 +48,10 @@ func (b *Block) Filter(filterAttribute FilterT[*Attribute], filterBlock FilterT[
if filterAttribute == nil || !filterAttribute(name, attrS) {
ret.Attributes[name] = attrS
}
if attrS.NestedType != nil {
ret.Attributes[name].NestedType = filterNestedType(attrS.NestedType, filterAttribute)
}
}
if b.BlockTypes != nil {
@ -55,3 +71,25 @@ func (b *Block) Filter(filterAttribute FilterT[*Attribute], filterBlock FilterT[
return ret
}
func filterNestedType(obj *Object, filterAttribute FilterT[*Attribute]) *Object {
if obj == nil {
return nil
}
ret := &Object{
Attributes: map[string]*Attribute{},
Nesting: obj.Nesting,
}
for name, attrS := range obj.Attributes {
if filterAttribute == nil || !filterAttribute(name, attrS) {
ret.Attributes[name] = attrS
if attrS.NestedType != nil {
ret.Attributes[name].NestedType = filterNestedType(attrS.NestedType, filterAttribute)
}
}
}
return ret
}

View File

@ -0,0 +1,278 @@
package configschema
import (
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestFilter(t *testing.T) {
testCases := map[string]struct {
schema *Block
filterAttribute FilterT[*Attribute]
filterBlock FilterT[*NestedBlock]
want *Block
}{
"empty": {
schema: &Block{},
filterAttribute: FilterDeprecatedAttribute,
filterBlock: FilterDeprecatedBlock,
want: &Block{},
},
"noop": {
schema: &Block{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
Required: true,
},
},
BlockTypes: map[string]*NestedBlock{
"list": {
Nesting: NestingList,
Block: Block{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
Required: true,
},
},
},
},
},
},
filterAttribute: nil,
filterBlock: nil,
want: &Block{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
Required: true,
},
},
BlockTypes: map[string]*NestedBlock{
"list": {
Nesting: NestingList,
Block: Block{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
Required: true,
},
},
},
},
},
},
},
"filter_deprecated": {
schema: &Block{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
Optional: true,
},
"deprecated_string": {
Type: cty.String,
Deprecated: true,
},
"nested": {
NestedType: &Object{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
},
"deprecated_string": {
Type: cty.String,
Deprecated: true,
},
},
Nesting: NestingList,
},
},
},
BlockTypes: map[string]*NestedBlock{
"list": {
Nesting: NestingList,
Block: Block{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
Optional: true,
},
},
Deprecated: true,
},
},
},
},
filterAttribute: FilterDeprecatedAttribute,
filterBlock: FilterDeprecatedBlock,
want: &Block{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
Optional: true,
},
"nested": {
NestedType: &Object{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
},
},
Nesting: NestingList,
},
},
},
},
},
"filter_read_only": {
schema: &Block{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
Optional: true,
},
"read_only_string": {
Type: cty.String,
Computed: true,
},
"nested": {
NestedType: &Object{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
Optional: true,
},
"read_only_string": {
Type: cty.String,
Computed: true,
},
"deeply_nested": {
NestedType: &Object{
Attributes: map[string]*Attribute{
"number": {
Type: cty.Number,
Required: true,
},
"read_only_number": {
Type: cty.Number,
Computed: true,
},
},
Nesting: NestingList,
},
},
},
Nesting: NestingList,
},
},
},
BlockTypes: map[string]*NestedBlock{
"list": {
Nesting: NestingList,
Block: Block{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
Optional: true,
},
"read_only_string": {
Type: cty.String,
Computed: true,
},
},
},
},
},
},
filterAttribute: FilterReadOnlyAttribute,
filterBlock: nil,
want: &Block{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
Optional: true,
},
"nested": {
NestedType: &Object{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
Optional: true,
},
"deeply_nested": {
NestedType: &Object{
Attributes: map[string]*Attribute{
"number": {
Type: cty.Number,
Required: true,
},
},
Nesting: NestingList,
},
},
},
Nesting: NestingList,
},
},
},
BlockTypes: map[string]*NestedBlock{
"list": {
Nesting: NestingList,
Block: Block{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
},
"filter_optional_computed_id": {
schema: &Block{
Attributes: map[string]*Attribute{
"id": {
Type: cty.String,
Optional: true,
Computed: true,
},
"string": {
Type: cty.String,
Optional: true,
Computed: true,
},
},
},
filterAttribute: FilterHelperSchemaIdAttribute,
filterBlock: nil,
want: &Block{
Attributes: map[string]*Attribute{
"string": {
Type: cty.String,
Optional: true,
Computed: true,
},
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got := tc.schema.Filter(tc.filterAttribute, tc.filterBlock)
if !cmp.Equal(got, tc.want, cmp.Comparer(cty.Type.Equals), cmpopts.EquateEmpty()) {
t.Fatal(cmp.Diff(got, tc.want, cmp.Comparer(cty.Type.Equals), cmpopts.EquateEmpty()))
}
})
}
}

View File

@ -77,5 +77,12 @@ func MaybeWriteGeneratedConfig(plan *plans.Plan, out string) (wroteConfig bool,
}
}
if wroteConfig {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Config generation is experimental",
"Generating configuration during import is currently experimental, and the generated configuration format may change in future versions."))
}
return wroteConfig, diags
}

View File

@ -4741,3 +4741,67 @@ import {
}
})
}
// config generation still succeeds even when planning fails
func TestContext2Plan_importResourceConfigGenWithError(t *testing.T) {
addr := mustResourceInstanceAddr("test_object.a")
m := testModuleInline(t, map[string]string{
"main.tf": `
import {
to = test_object.a
id = "123"
}
`,
})
p := simpleMockProvider()
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{
PlannedState: cty.NullVal(cty.DynamicPseudoType),
Diagnostics: tfdiags.Diagnostics(nil).Append(errors.New("plan failed")),
}
p.ReadResourceResponse = &providers.ReadResourceResponse{
NewState: cty.ObjectVal(map[string]cty.Value{
"test_string": cty.StringVal("foo"),
}),
}
p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{
ImportedResources: []providers.ImportedResource{
{
TypeName: "test_object",
State: cty.ObjectVal(map[string]cty.Value{
"test_string": cty.StringVal("foo"),
}),
},
},
}
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
GenerateConfig: true,
})
if !diags.HasErrors() {
t.Fatal("expected error")
}
instPlan := plan.Changes.ResourceInstance(addr)
if instPlan == nil {
t.Fatalf("no plan for %s at all", addr)
}
want := `resource "test_object" "a" {
test_bool = null
test_list = null
test_map = null
test_number = null
test_string = "foo"
}`
got := instPlan.GeneratedConfig
if diff := cmp.Diff(want, got); len(diff) > 0 {
t.Errorf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
}
}

View File

@ -9,14 +9,12 @@ import (
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/checks"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/genconfig"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/plans/objchange"
@ -41,6 +39,10 @@ type NodeAbstractResourceInstance struct {
Dependencies []addrs.ConfigResource
preDestroyRefresh bool
// During import we may generate configuration for a resource, which needs
// to be stored in the final change.
generatedConfigHCL string
}
// NewNodeAbstractResourceInstance creates an abstract resource instance graph
@ -653,74 +655,26 @@ func (n *NodeAbstractResourceInstance) plan(
forceReplace []addrs.AbsResourceInstance,
) (*plans.ResourceInstanceChange, *states.ResourceInstanceObject, instances.RepetitionData, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var state *states.ResourceInstanceObject
var plan *plans.ResourceInstanceChange
var keyData instances.RepetitionData
var generatedConfig *configs.Resource
resource := n.Addr.Resource.Resource
provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
if err != nil {
return plan, state, keyData, diags.Append(err)
return nil, nil, keyData, diags.Append(err)
}
if providerSchema == nil {
diags = diags.Append(fmt.Errorf("provider schema is unavailable for %s", n.Addr))
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
schema, _ := providerSchema.SchemaForResourceAddr(resource)
if schema == nil {
// Should be caught during validation, so we don't bother with a pretty error here
diags = diags.Append(fmt.Errorf("provider does not support resource type %q", resource.Type))
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
// If we're importing and generating config, generate it now.
var generatedHCL string
if n.generateConfig {
var generatedDiags tfdiags.Diagnostics
if n.Config != nil {
return plan, state, keyData, diags.Append(fmt.Errorf("tried to generate config for %s, but it already exists", n.Addr))
}
// 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, currentState, schema)
diags = diags.Append(generatedDiags)
generatedHCL = genconfig.WrapResourceContents(n.Addr, generatedHCLAttributes)
// parse the "file" as HCL to get the hcl.Body
synthHCLFile, hclDiags := hclsyntax.ParseConfig([]byte(generatedHCLAttributes), "generated_resources.tf", hcl.Pos{Byte: 0, Line: 1, Column: 1})
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
return plan, state, keyData, diags
}
// We have to do a kind of mini parsing of the content here to correctly
// mark attributes like 'provider' as hidden. We only care about the
// resulting content, so it's remain that gets passed into the resource
// as the config.
_, remain, resourceDiags := synthHCLFile.Body.PartialContent(configs.ResourceBlockSchema)
diags = diags.Append(resourceDiags)
if resourceDiags.HasErrors() {
return plan, state, keyData, diags
}
generatedConfig = &configs.Resource{
Mode: addrs.ManagedResourceMode,
Type: n.Addr.Resource.Resource.Type,
Name: n.Addr.Resource.Resource.Name,
Config: remain,
Managed: &configs.ManagedResource{},
Provider: n.ResolvedProvider.Provider,
}
n.Config = generatedConfig
}
if n.Config == nil {
// This shouldn't happen. A node that isn't generating config should
// have embedded config, and the rest of Terraform should enforce this.
@ -731,7 +685,7 @@ func (n *NodeAbstractResourceInstance) plan(
tfdiags.Error,
"Resource has no configuration",
fmt.Sprintf("Terraform attempted to process a resource at %s that has no configuration. This is a bug in Terraform; please report it!", n.Addr.String())))
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
config := *n.Config
@ -747,7 +701,6 @@ func (n *NodeAbstractResourceInstance) plan(
}
// Evaluate the configuration
forEach, _ := evaluateForEachExpression(n.Config.ForEach, ctx)
keyData = EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach)
@ -760,7 +713,7 @@ func (n *NodeAbstractResourceInstance) plan(
)
diags = diags.Append(checkDiags)
if diags.HasErrors() {
return plan, state, keyData, diags // failed preconditions prevent further evaluation
return nil, nil, keyData, diags // failed preconditions prevent further evaluation
}
// If we have a previous plan and the action was a noop, then the only
@ -773,13 +726,13 @@ func (n *NodeAbstractResourceInstance) plan(
origConfigVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
metaConfigVal, metaDiags := n.providerMetas(ctx)
diags = diags.Append(metaDiags)
if diags.HasErrors() {
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
var priorVal cty.Value
@ -822,7 +775,7 @@ func (n *NodeAbstractResourceInstance) plan(
)
diags = diags.Append(validateResp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
if diags.HasErrors() {
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
// ignore_changes is meant to only apply to the configuration, so it must
@ -835,7 +788,7 @@ func (n *NodeAbstractResourceInstance) plan(
configValIgnored, ignoreChangeDiags := n.processIgnoreChanges(priorVal, origConfigVal, schema)
diags = diags.Append(ignoreChangeDiags)
if ignoreChangeDiags.HasErrors() {
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
// Create an unmarked version of our config val and our prior val.
@ -851,7 +804,7 @@ func (n *NodeAbstractResourceInstance) plan(
return h.PreDiff(n.Addr, states.CurrentGen, priorVal, proposedNewVal)
}))
if diags.HasErrors() {
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{
@ -864,7 +817,7 @@ func (n *NodeAbstractResourceInstance) plan(
})
diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
if diags.HasErrors() {
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
plannedNewVal := resp.PlannedState
@ -892,7 +845,7 @@ func (n *NodeAbstractResourceInstance) plan(
))
}
if diags.HasErrors() {
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
if errs := objchange.AssertPlanValid(schema, unmarkedPriorVal, unmarkedConfigVal, plannedNewVal); len(errs) > 0 {
@ -922,7 +875,7 @@ func (n *NodeAbstractResourceInstance) plan(
),
))
}
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
}
@ -942,7 +895,7 @@ func (n *NodeAbstractResourceInstance) plan(
plannedNewVal, ignoreChangeDiags = n.processIgnoreChanges(unmarkedPriorVal, plannedNewVal, nil)
diags = diags.Append(ignoreChangeDiags)
if ignoreChangeDiags.HasErrors() {
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
}
@ -1009,7 +962,7 @@ func (n *NodeAbstractResourceInstance) plan(
}
}
if diags.HasErrors() {
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
}
@ -1103,7 +1056,7 @@ func (n *NodeAbstractResourceInstance) plan(
// append these new diagnostics if there's at least one error inside.
if resp.Diagnostics.HasErrors() {
diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
plannedNewVal = resp.PlannedState
plannedPrivate = resp.PlannedPrivate
@ -1123,7 +1076,7 @@ func (n *NodeAbstractResourceInstance) plan(
))
}
if diags.HasErrors() {
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
}
@ -1173,11 +1126,11 @@ func (n *NodeAbstractResourceInstance) plan(
return h.PostDiff(n.Addr, states.CurrentGen, action, priorVal, plannedNewVal)
}))
if diags.HasErrors() {
return plan, state, keyData, diags
return nil, nil, keyData, diags
}
// Update our return plan
plan = &plans.ResourceInstanceChange{
plan := &plans.ResourceInstanceChange{
Addr: n.Addr,
PrevRunAddr: n.prevRunAddr(ctx),
Private: plannedPrivate,
@ -1189,14 +1142,14 @@ func (n *NodeAbstractResourceInstance) plan(
// to propogate through evaluation.
// Marks will be removed when encoding.
After: plannedNewVal,
GeneratedConfig: generatedHCL,
GeneratedConfig: n.generatedConfigHCL,
},
ActionReason: actionReason,
RequiredReplace: reqRep,
}
// Update our return state
state = &states.ResourceInstanceObject{
state := &states.ResourceInstanceObject{
// We use the special "planned" status here to note that this
// object's value is not yet complete. Objects with this status
// cannot be used during expression evaluation, so the caller
@ -1211,21 +1164,6 @@ func (n *NodeAbstractResourceInstance) plan(
return plan, state, keyData, diags
}
// generateHCLStringAttributes produces a string in HCL format for the given
// resource state and schema without the surrounding block.
func (n *NodeAbstractResource) generateHCLStringAttributes(addr addrs.AbsResourceInstance, state *states.ResourceInstanceObject, schema *configschema.Block) (string, tfdiags.Diagnostics) {
filteredSchema := schema.Filter(
configschema.FilterOr(configschema.FilterReadOnlyAttributes, configschema.FilterDeprecatedAttribute),
configschema.FilterDeprecatedBlock)
providerAddr := addrs.LocalProviderConfig{
LocalName: n.ResolvedProvider.Provider.Type,
Alias: n.ResolvedProvider.Alias,
}
return genconfig.GenerateResourceContents(addr, filteredSchema, providerAddr, state.Value)
}
func (n *NodeAbstractResource) processIgnoreChanges(prior, config cty.Value, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) {
// ignore_changes only applies when an object already exists, since we
// can't ignore changes to a thing we've not created yet.

View File

@ -9,9 +9,13 @@ import (
"sort"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/genconfig"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
@ -134,7 +138,6 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
config := n.Config
addr := n.ResourceInstanceAddr()
var change *plans.ResourceInstanceChange
var instanceRefreshState *states.ResourceInstanceObject
checkRuleSeverity := tfdiags.Error
@ -183,7 +186,7 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
// If the resource is to be imported, we now ask the provider for an Import
// and a Refresh, and save the resulting state to instanceRefreshState.
if importing {
instanceRefreshState, diags = n.importState(ctx, addr, provider)
instanceRefreshState, diags = n.importState(ctx, addr, provider, providerSchema)
} else {
var readDiags tfdiags.Diagnostics
instanceRefreshState, readDiags = n.readResourceInstanceState(ctx, addr)
@ -265,10 +268,30 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
}
change, instancePlanState, repeatData, planDiags := n.plan(
ctx, change, instanceRefreshState, n.ForceCreateBeforeDestroy, n.forceReplace,
ctx, nil, instanceRefreshState, n.ForceCreateBeforeDestroy, n.forceReplace,
)
diags = diags.Append(planDiags)
if diags.HasErrors() {
// If we are importing and generating a configuration, we need to
// ensure the change is written out so the configuration can be
// captured.
if n.generateConfig {
// Update our return plan
change := &plans.ResourceInstanceChange{
Addr: n.Addr,
PrevRunAddr: n.prevRunAddr(ctx),
ProviderAddr: n.ResolvedProvider,
Change: plans.Change{
// we only need a placeholder, so this will be a NoOp
Action: plans.NoOp,
Before: instanceRefreshState.Value,
After: instanceRefreshState.Value,
GeneratedConfig: n.generatedConfigHCL,
},
}
diags = diags.Append(n.writeChange(ctx, change, ""))
}
return diags
}
@ -415,7 +438,7 @@ func (n *NodePlannableResourceInstance) replaceTriggered(ctx EvalContext, repDat
return diags
}
func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.AbsResourceInstance, provider providers.Interface) (*states.ResourceInstanceObject, tfdiags.Diagnostics) {
func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.AbsResourceInstance, provider providers.Interface, providerSchema *ProviderSchema) (*states.ResourceInstanceObject, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
absAddr := addr.Resource.Absolute(ctx.Path())
@ -519,10 +542,90 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.
return instanceRefreshState, diags
}
// If we're importing and generating config, generate it now.
if n.generateConfig {
if n.Config != nil {
return instanceRefreshState, diags.Append(fmt.Errorf("tried to generate config for %s, but it already exists", n.Addr))
}
schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.Resource.Resource)
if schema == nil {
// Should be caught during validation, so we don't bother with a pretty error here
diags = diags.Append(fmt.Errorf("provider does not support resource type for %q", n.Addr))
return instanceRefreshState, diags
}
// 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)
diags = diags.Append(generatedDiags)
n.generatedConfigHCL = genconfig.WrapResourceContents(n.Addr, generatedHCLAttributes)
// parse the "file" as HCL to get the hcl.Body
synthHCLFile, hclDiags := hclsyntax.ParseConfig([]byte(generatedHCLAttributes), "generated_resources.tf", hcl.Pos{Byte: 0, Line: 1, Column: 1})
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
return instanceRefreshState, diags
}
// We have to do a kind of mini parsing of the content here to correctly
// mark attributes like 'provider' as hidden. We only care about the
// resulting content, so it's remain that gets passed into the resource
// as the config.
_, remain, resourceDiags := synthHCLFile.Body.PartialContent(configs.ResourceBlockSchema)
diags = diags.Append(resourceDiags)
if resourceDiags.HasErrors() {
return instanceRefreshState, diags
}
n.Config = &configs.Resource{
Mode: addrs.ManagedResourceMode,
Type: n.Addr.Resource.Resource.Type,
Name: n.Addr.Resource.Resource.Name,
Config: remain,
Managed: &configs.ManagedResource{},
Provider: n.ResolvedProvider.Provider,
}
}
diags = diags.Append(riNode.writeResourceInstanceState(ctx, instanceRefreshState, refreshState))
return instanceRefreshState, 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(
configschema.FilterOr(
configschema.FilterReadOnlyAttribute,
configschema.FilterDeprecatedAttribute,
// The legacy SDK adds an Optional+Computed "id" attribute to the
// resource schema even if not defined in provider code.
// During validation, however, the presence of an extraneous "id"
// attribute in config will cause an error.
// Remove this attribute so we do not generate an "id" attribute
// where there is a risk that it is not in the real resource schema.
//
// TRADEOFF: Resources in which there actually is an
// Optional+Computed "id" attribute in the schema will have that
// attribute missing from generated config.
configschema.FilterHelperSchemaIdAttribute,
),
configschema.FilterDeprecatedBlock,
)
providerAddr := addrs.LocalProviderConfig{
LocalName: n.ResolvedProvider.Provider.Type,
Alias: n.ResolvedProvider.Alias,
}
return genconfig.GenerateResourceContents(addr, filteredSchema, providerAddr, state.Value)
}
// mergeDeps returns the union of 2 sets of dependencies
func mergeDeps(a, b []addrs.ConfigResource) []addrs.ConfigResource {
switch {