diff --git a/internal/command/testdata/plan-import-config-gen/generated.tf.expected b/internal/command/testdata/plan-import-config-gen/generated.tf.expected index bfdfc3ca69..c864b2a600 100644 --- a/internal/command/testdata/plan-import-config-gen/generated.tf.expected +++ b/internal/command/testdata/plan-import-config-gen/generated.tf.expected @@ -4,5 +4,4 @@ # __generated__ by Terraform from "bar" resource "test_instance" "foo" { ami = null - id = "bar" } diff --git a/internal/configs/configschema/filter.go b/internal/configs/configschema/filter.go index 40496ca8c3..bb0f3d8b48 100644 --- a/internal/configs/configschema/filter.go +++ b/internal/configs/configschema/filter.go @@ -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 +} diff --git a/internal/configs/configschema/filter_test.go b/internal/configs/configschema/filter_test.go new file mode 100644 index 0000000000..29b6115d62 --- /dev/null +++ b/internal/configs/configschema/filter_test.go @@ -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())) + } + }) + } +} diff --git a/internal/genconfig/generate_config_write.go b/internal/genconfig/generate_config_write.go index c82fa3a294..c068c358c7 100644 --- a/internal/genconfig/generate_config_write.go +++ b/internal/genconfig/generate_config_write.go @@ -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 } diff --git a/internal/terraform/context_plan2_test.go b/internal/terraform/context_plan2_test.go index a51d975747..3d03108cc3 100644 --- a/internal/terraform/context_plan2_test.go +++ b/internal/terraform/context_plan2_test.go @@ -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) + } +} diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index 8a6a21f25c..790efb5db3 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -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. diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index 12691ee91c..af9611a55b 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -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 {