jsonformat: render deferred actions

pull/35542/head
Daniel Schmidt 2 years ago
parent 24833a2b06
commit 4afb3b2b48
No known key found for this signature in database
GPG Key ID: 377C3A4D62FBBBE2

@ -62,6 +62,18 @@ func precomputeDiffs(plan Plan, mode plans.Mode) diffs {
})
}
for _, change := range plan.DeferredChanges {
schema := plan.getSchema(change.ResourceChange)
structuredChange := structured.FromJsonChange(change.ResourceChange.Change, attribute_path.AlwaysMatcher())
diffs.deferred = append(diffs.deferred, deferredDiff{
reason: change.Reason,
diff: diff{
change: change.ResourceChange,
diff: differ.ComputeDiffForBlock(structuredChange, schema.Block),
},
})
}
for key, output := range plan.OutputChanges {
change := structured.FromJsonChange(output, attribute_path.AlwaysMatcher())
diffs.outputs[key] = differ.ComputeDiffForOutput(change)
@ -71,9 +83,10 @@ func precomputeDiffs(plan Plan, mode plans.Mode) diffs {
}
type diffs struct {
drift []diff
changes []diff
outputs map[string]computed.Diff
drift []diff
changes []diff
deferred []deferredDiff
outputs map[string]computed.Diff
}
func (d diffs) Empty() bool {
@ -104,3 +117,8 @@ func (d diff) Moved() bool {
func (d diff) Importing() bool {
return d.change.Change.Importing != nil
}
type deferredDiff struct {
diff diff
reason string
}

@ -25,11 +25,12 @@ const (
)
type Plan struct {
PlanFormatVersion string `json:"plan_format_version"`
OutputChanges map[string]jsonplan.Change `json:"output_changes,omitempty"`
ResourceChanges []jsonplan.ResourceChange `json:"resource_changes,omitempty"`
ResourceDrift []jsonplan.ResourceChange `json:"resource_drift,omitempty"`
RelevantAttributes []jsonplan.ResourceAttr `json:"relevant_attributes,omitempty"`
PlanFormatVersion string `json:"plan_format_version"`
OutputChanges map[string]jsonplan.Change `json:"output_changes,omitempty"`
ResourceChanges []jsonplan.ResourceChange `json:"resource_changes,omitempty"`
ResourceDrift []jsonplan.ResourceChange `json:"resource_drift,omitempty"`
RelevantAttributes []jsonplan.ResourceAttr `json:"relevant_attributes,omitempty"`
DeferredChanges []jsonplan.DeferredResourceChange `json:"deferred_changes,omitempty"`
ProviderFormatVersion string `json:"provider_format_version"`
ProviderSchemas map[string]*jsonprovider.Provider `json:"provider_schemas,omitempty"`
@ -180,6 +181,12 @@ func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...plans.Q
renderer.Streams.Println()
}
haveDeferredChanges := renderHumanDeferredChanges(renderer, diffs, mode)
if haveDeferredChanges {
renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns()))
renderer.Streams.Println()
}
if willPrintResourceChanges {
renderer.Streams.Println(format.WordWrap(
"\nTerraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:",
@ -334,6 +341,24 @@ func renderHumanDiffDrift(renderer Renderer, diffs diffs, mode plans.Mode) bool
return true
}
func renderHumanDeferredChanges(renderer Renderer, diffs diffs, mode plans.Mode) bool {
if len(diffs.deferred) == 0 {
return false
}
renderer.Streams.Print(renderer.Colorize.Color("\n[bold][cyan]Note:[reset][bold] This is a partial plan, parts can only be known in the next plan / apply cycle.\n"))
renderer.Streams.Println()
for _, deferred := range diffs.deferred {
diff, render := renderHumanDeferredDiff(renderer, deferred)
if render {
renderer.Streams.Println()
renderer.Streams.Println(diff)
}
}
return true
}
func renderHumanDiff(renderer Renderer, diff diff, cause string) (string, bool) {
// Internally, our computed diffs can't tell the difference between a
@ -359,6 +384,47 @@ func renderHumanDiff(renderer Renderer, diff diff, cause string) (string, bool)
return buf.String(), true
}
func renderHumanDeferredDiff(renderer Renderer, deferred deferredDiff) (string, bool) {
// Internally, our computed diffs can't tell the difference between a
// replace action (eg. CreateThenDestroy, DestroyThenCreate) and a simple
// update action. So, at the top most level we rely on the action provided
// by the plan itself instead of what we compute. Nested attributes and
// blocks however don't have the replace type of actions, so we can trust
// the computed actions of these.
action := jsonplan.UnmarshalActions(deferred.diff.change.Change.Actions)
if action == plans.NoOp && !deferred.diff.Moved() && !deferred.diff.Importing() {
// Skip resource changes that have nothing interesting to say.
return "", false
}
var buf bytes.Buffer
var explanation string
switch deferred.reason {
// TODO: Add other cases
case jsonplan.DeferredReasonInstanceCountUnknown:
explanation = "The number of resource instances is unknown."
case jsonplan.DeferredReasonResourceConfigUnknown:
explanation = "The resource configuration is unknown."
case jsonplan.DeferredReasonProviderConfigUnknown:
explanation = "The provider configuration is unknown."
case jsonplan.DeferredReasonDeferredPrereq:
explanation = "A prerequisite for this resource is deferred."
case jsonplan.DeferredReasonAbsentPrereq:
explanation = "A prerequisite for this resource is absent."
default:
explanation = "Unknown / Not supported by this version of Terraform."
}
buf.WriteString(renderer.Colorize.Color(fmt.Sprintf("%s was deferred: \nReason: %s\n", deferred.diff.change.Address, explanation)))
opts := computed.NewRenderHumanOpts(renderer.Colorize)
opts.ShowUnchangedChildren = deferred.diff.Importing()
buf.WriteString(fmt.Sprintf("%s %s %s", renderer.Colorize.Color(format.DiffActionSymbol(action)), resourceChangeHeader(deferred.diff.change), deferred.diff.diff.RenderHuman(0, opts)))
return buf.String(), true
}
func resourceChangeComment(resource jsonplan.ResourceChange, action plans.Action, changeCause string) string {
var buf bytes.Buffer

@ -7127,6 +7127,236 @@ func TestOutputChanges(t *testing.T) {
}
}
func TestResourceChange_deferredActions(t *testing.T) {
color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
providerAddr := addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
}
testCases := map[string]struct {
changes []*plans.DeferredResourceInstanceChange
output string
}{
"deferred create action": {
changes: []*plans.DeferredResourceInstanceChange{
{
DeferredReason: providers.DeferredReasonAbsentPrereq,
Change: &plans.ResourceInstanceChange{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "instance",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: providerAddr,
Change: plans.Change{
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"ami": cty.StringVal("bar"),
"disk": cty.UnknownVal(cty.Object(map[string]cty.Type{
"mount_point": cty.String,
"size": cty.Number,
})),
"root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{
"volume_type": cty.String,
})),
}),
},
},
},
},
output: `test_instance.instance was deferred:
Reason: A prerequisite for this resource is absent.
+ resource "test_instance" "instance" {
+ ami = "bar"
+ disk = (known after apply)
+ id = (known after apply)
+ root_block_device (known after apply)
}`,
},
"deferred create action unknown for_each": {
changes: []*plans.DeferredResourceInstanceChange{
{
DeferredReason: providers.DeferredReasonInstanceCountUnknown,
Change: &plans.ResourceInstanceChange{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "instance",
}.Instance(addrs.WildcardKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: providerAddr,
Change: plans.Change{
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"ami": cty.StringVal("bar"),
"disk": cty.UnknownVal(cty.Object(map[string]cty.Type{
"mount_point": cty.String,
"size": cty.Number,
})),
"root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{
"volume_type": cty.String,
})),
}),
},
},
},
},
output: `test_instance.instance[*] was deferred:
Reason: The number of resource instances is unknown.
+ resource "test_instance" "instance" {
+ ami = "bar"
+ disk = (known after apply)
+ id = (known after apply)
+ root_block_device (known after apply)
}`,
},
"deferred update action": {
changes: []*plans.DeferredResourceInstanceChange{
{
DeferredReason: providers.DeferredReasonProviderConfigUnknown,
Change: &plans.ResourceInstanceChange{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "instance",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: providerAddr,
Change: plans.Change{
Action: plans.Update,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo"),
"ami": cty.StringVal("bar"),
"disk": cty.NullVal(cty.Object(map[string]cty.Type{
"mount_point": cty.String,
"size": cty.Number,
})),
"root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{
"volume_type": cty.String,
})),
}),
After: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo"),
"ami": cty.StringVal("baz"),
"disk": cty.NullVal(cty.Object(map[string]cty.Type{
"mount_point": cty.String,
"size": cty.Number,
})),
"root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{
"volume_type": cty.String,
})),
}),
},
},
},
},
output: `test_instance.instance was deferred:
Reason: The provider configuration is unknown.
~ resource "test_instance" "instance" {
~ ami = "bar" -> "baz"
id = "foo"
}`,
},
"deferred destroy action": {
changes: []*plans.DeferredResourceInstanceChange{
{
DeferredReason: providers.DeferredReasonResourceConfigUnknown,
Change: &plans.ResourceInstanceChange{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "instance",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: providerAddr,
Change: plans.Change{
Action: plans.Update,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo"),
"ami": cty.StringVal("bar"),
"disk": cty.NullVal(cty.Object(map[string]cty.Type{
"mount_point": cty.String,
"size": cty.Number,
})),
"root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{
"volume_type": cty.String,
})),
}),
After: cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"ami": cty.String,
"disk": cty.Object(map[string]cty.Type{
"mount_point": cty.String,
"size": cty.Number,
}),
"root_block_device": cty.Object(map[string]cty.Type{
"volume_type": cty.String,
}),
})),
},
},
},
},
output: `test_instance.instance was deferred:
Reason: The resource configuration is unknown.
~ resource "test_instance" "instance" {
- ami = "bar" -> null
- id = "foo" -> null
}`,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
blockSchema := testSchema(configschema.NestingSingle)
fullSchema := &terraform.Schemas{
Providers: map[addrs.Provider]providers.ProviderSchema{
providerAddr.Provider: {
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: blockSchema,
},
},
},
},
}
var changes []*plans.DeferredResourceInstanceChangeSrc
for _, change := range tc.changes {
changeSrc, err := change.Encode(blockSchema.ImpliedType())
if err != nil {
t.Fatalf("Failed to encode change: %s", err)
}
changes = append(changes, changeSrc)
}
deferredChanges, err := jsonplan.MarshalDeferredResourceChanges(changes, fullSchema)
if err != nil {
t.Fatalf("failed to marshal deferred changes: %s", err)
}
renderer := Renderer{Colorize: color}
jsonschemas := jsonprovider.MarshalForRenderer(fullSchema)
diffs := precomputeDiffs(Plan{
DeferredChanges: deferredChanges,
ProviderSchemas: jsonschemas,
}, plans.NormalMode)
// TODO: Add diffing for outputs
// TODO: Make sure it's true and either make it a single entity in the test case or deal with a list here
output, _ := renderHumanDeferredDiff(renderer, diffs.deferred[0])
if diff := cmp.Diff(tc.output, output); len(diff) > 0 {
t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", output, tc.output, diff)
}
})
}
}
func outputChange(name string, before, after cty.Value, sensitive bool) *plans.OutputChangeSrc {
addr := addrs.AbsOutputValue{
OutputValue: addrs.OutputValue{Name: name},

@ -292,7 +292,7 @@ func Marshal(
}
if p.DeferredResources != nil {
output.DeferredChanges, err = marshalDeferredResourceChanges(p.DeferredResources, schemas)
output.DeferredChanges, err = MarshalDeferredResourceChanges(p.DeferredResources, schemas)
if err != nil {
return nil, fmt.Errorf("error in marshaling deferred resource changes: %s", err)
}
@ -583,10 +583,11 @@ func marshalResourceChange(rc *plans.ResourceInstanceChangeSrc, schemas *terrafo
return r, nil
}
// marshalDeferredResourceChanges converts the provided internal representation
// MarshalDeferredResourceChanges converts the provided internal representation
// of DeferredResourceInstanceChangeSrc objects into the public structured JSON
// changes.
func marshalDeferredResourceChanges(resources []*plans.DeferredResourceInstanceChangeSrc, schemas *terraform.Schemas) ([]DeferredResourceChange, error) {
// This is public to make testing easier.
func MarshalDeferredResourceChanges(resources []*plans.DeferredResourceInstanceChangeSrc, schemas *terraform.Schemas) ([]DeferredResourceChange, error) {
var ret []DeferredResourceChange
var sortedResources []*plans.DeferredResourceInstanceChangeSrc

Loading…
Cancel
Save