mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
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:
parent
f6737d47e7
commit
be2ad69eda
@ -4,5 +4,4 @@
|
||||
# __generated__ by Terraform from "bar"
|
||||
resource "test_instance" "foo" {
|
||||
ami = null
|
||||
id = "bar"
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
278
internal/configs/configschema/filter_test.go
Normal file
278
internal/configs/configschema/filter_test.go
Normal 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()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user