actions: render invoked actions (#37540)

* actions: add invoke graph nodes

* fix small bugs

* add additional tests

* remove whitespace

* actions: invoke rendered actions

* also fix up apply message

* make description headers consistent

* fix copyright headers

* go generate
pull/37543/head
Liam Cervante 9 months ago committed by GitHub
parent b0ff7c271c
commit 673717d01a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -145,13 +145,19 @@ func (b *Local) opApply(
desc = "Terraform will destroy all your managed infrastructure, as shown above.\n" +
"There is no undo. Only 'yes' will be accepted to confirm."
case plans.RefreshOnlyMode:
if op.Workspace != "default" {
query = "Would you like to update the Terraform state for \"" + op.Workspace + "\" to reflect these detected changes?"
if len(plan.ActionTargetAddrs) > 0 {
query = "Would you like to invoke the specified actions?"
desc = "Terraform will invoke the actions described above, and any changes will be written to the state without modifying real infrastructure\n" +
"There is no undo. Only 'yes' will be accepted to confirm."
} else {
query = "Would you like to update the Terraform state to reflect these detected changes?"
if op.Workspace != "default" {
query = "Would you like to update the Terraform state for \"" + op.Workspace + "\" to reflect these detected changes?"
} else {
query = "Would you like to update the Terraform state to reflect these detected changes?"
}
desc = "Terraform will write these changes to the state without modifying any real infrastructure.\n" +
"There is no undo. Only 'yes' will be accepted to confirm."
}
desc = "Terraform will write these changes to the state without modifying any real infrastructure.\n" +
"There is no undo. Only 'yes' will be accepted to confirm."
default:
if op.Workspace != "default" {
query = "Do you want to perform these actions in workspace \"" + op.Workspace + "\"?"

@ -84,8 +84,8 @@ func precomputeDiffs(plan Plan, mode plans.Mode) diffs {
slices.SortFunc(before, jsonplan.ActionInvocationCompare)
slices.SortFunc(after, jsonplan.ActionInvocationCompare)
beforeActionsTriggered := []actionInvocation{}
afterActionsTriggered := []actionInvocation{}
var beforeActionsTriggered []actionInvocation
var afterActionsTriggered []actionInvocation
for _, action := range before {
schema := plan.getActionSchema(action)
beforeActionsTriggered = append(beforeActionsTriggered, actionInvocation{
@ -109,6 +109,17 @@ func precomputeDiffs(plan Plan, mode plans.Mode) diffs {
})
}
for _, action := range plan.ActionInvocations {
if action.InvokeActionTrigger == nil {
// lifecycle actions are handled within the resource
continue
}
diffs.actions = append(diffs.actions, actionInvocation{
invocation: action,
schema: plan.getActionSchema(action),
})
}
for _, change := range plan.DeferredChanges {
schema := plan.getSchema(change.ResourceChange)
structuredChange := structured.FromJsonChange(change.ResourceChange.Change, attribute_path.AlwaysMatcher())
@ -133,6 +144,7 @@ type diffs struct {
drift []diff
changes []diff
deferred []deferredDiff
actions []actionInvocation
outputs map[string]computed.Diff
}

@ -94,9 +94,9 @@ func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...plans.Q
// Precompute the outputs and actions early, so we can make a decision about whether we
// display the "there are no changes messages".
outputs := renderHumanDiffOutputs(renderer, diffs.outputs)
actions := renderHumanActionInvocations(renderer, plan.ActionInvocations)
actions, actionCount := renderHumanActionInvocations(renderer, diffs.actions)
if len(changes) == 0 && len(outputs) == 0 && len(actions) == 0 {
if len(changes) == 0 && len(outputs) == 0 && actionCount == 0 {
// If we didn't find any changes to report at all then this is a
// "No changes" plan. How we'll present this depends on whether
// the plan is "applyable" and, if so, whether it had refresh changes
@ -256,7 +256,7 @@ func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...plans.Q
}
if len(actions) > 0 {
renderer.Streams.Print("\nActions to be invoked:\n")
renderer.Streams.Print(renderer.Colorize.Color("\nTerraform will invoke the following action(s):\n\n"))
renderer.Streams.Printf("%s\n", actions)
}
@ -502,8 +502,13 @@ func renderHumanDeferredDiff(renderer Renderer, deferred deferredDiff) (string,
// All actions that run based on the resource lifecycle should be rendered as part of the resource
// changes, therefore this function only renders actions that are invoked by the CLI
func renderHumanActionInvocations(renderer Renderer, actionInvocations []jsonplan.ActionInvocation) string {
return "" // TODO: We will use this function once we support CLI invoked actions.
func renderHumanActionInvocations(renderer Renderer, actionInvocations []actionInvocation) (string, int) {
var invocations []string
for _, invocation := range actionInvocations {
header := fmt.Sprintf(renderer.Colorize.Color(" [bold]# %s[reset] will be invoked"), invocation.invocation.Address)
invocations = append(invocations, fmt.Sprintf("%s\n%s", header, renderActionInvocation(renderer, invocation)))
}
return strings.Join(invocations, "\n"), len(invocations)
}
func resourceChangeComment(resource jsonplan.ResourceChange, action plans.Action, changeCause string) string {

@ -27,6 +27,175 @@ import (
"github.com/hashicorp/terraform/internal/terraform"
)
func TestRenderHuman_InvokeActionPlan(t *testing.T) {
color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
streams, done := terminal.StreamsForTesting(t)
plan := Plan{
ActionInvocations: []jsonplan.ActionInvocation{
{
Address: "action.test_action.action",
Type: "test_action",
Name: "action",
ConfigValues: map[string]json.RawMessage{
"attr": []byte("\"one\""),
},
ConfigSensitive: nil,
ProviderName: "test",
InvokeActionTrigger: new(jsonplan.InvokeActionTrigger),
},
},
ProviderSchemas: map[string]*jsonprovider.Provider{
"test": {
ActionSchemas: map[string]*jsonprovider.ActionSchema{
"test_action": {
ConfigSchema: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"attr": {
AttributeType: []byte("\"string\""),
},
},
},
Unlinked: new(jsonprovider.UnlinkedAction),
},
},
},
},
}
renderer := Renderer{Colorize: color, Streams: streams}
plan.renderHuman(renderer, plans.RefreshOnlyMode)
want := `
Terraform will invoke the following action(s):
# action.test_action.action will be invoked
action "test_action" "action" {
config {
attr = "one"
}
}
`
got := done(t).Stdout()
if diff := cmp.Diff(want, got); len(diff) > 0 {
t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
}
}
func TestRenderHuman_InvokeActionPlanWithRefresh(t *testing.T) {
color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
streams, done := terminal.StreamsForTesting(t)
plan := Plan{
ActionInvocations: []jsonplan.ActionInvocation{
{
Address: "action.test_action.action",
Type: "test_action",
Name: "action",
ConfigValues: map[string]json.RawMessage{
"attr": []byte("\"one\""),
},
ConfigSensitive: nil,
ProviderName: "test",
InvokeActionTrigger: new(jsonplan.InvokeActionTrigger),
},
},
ResourceDrift: []jsonplan.ResourceChange{
{
Address: "aws_instance.foo",
Mode: "managed",
Type: "aws_instance",
Name: "foo",
IndexUnknown: true,
ProviderName: "aws",
Change: jsonplan.Change{
Actions: []string{"update"},
Before: marshalJson(t, map[string]interface{}{
"id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
"value": "Hello, World!",
}),
After: marshalJson(t, map[string]interface{}{
"id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
"value": "Hello, World!",
}),
},
},
},
ProviderSchemas: map[string]*jsonprovider.Provider{
"test": {
ActionSchemas: map[string]*jsonprovider.ActionSchema{
"test_action": {
ConfigSchema: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"attr": {
AttributeType: []byte("\"string\""),
},
},
},
Unlinked: new(jsonprovider.UnlinkedAction),
},
},
},
"aws": {
ResourceSchemas: map[string]*jsonprovider.Schema{
"aws_instance": {
Block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"id": {
AttributeType: marshalJson(t, "string"),
},
"ami": {
AttributeType: marshalJson(t, "string"),
},
},
},
},
},
},
},
}
renderer := Renderer{Colorize: color, Streams: streams}
plan.renderHuman(renderer, plans.RefreshOnlyMode)
want := `
Note: Objects have changed outside of Terraform
Terraform detected the following changes made outside of Terraform since the
last "terraform apply" which may have affected this plan:
# aws_instance.foo has changed
~ resource "aws_instance" "foo" {
id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
}
This is a refresh-only plan, so Terraform will not take any actions to undo
these. If you were expecting these changes then you can apply this plan to
record the updated values in the Terraform state without changing any remote
objects.
Terraform will invoke the following action(s):
# action.test_action.action will be invoked
action "test_action" "action" {
config {
attr = "one"
}
}
`
got := done(t).Stdout()
if diff := cmp.Diff(want, got); len(diff) > 0 {
t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
}
}
func TestRenderHuman_EmptyPlan(t *testing.T) {
color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
streams, done := terminal.StreamsForTesting(t)

Loading…
Cancel
Save