[Plannable Import] Implement streamed logs for plan (#33106)

* [plannable import] embed the resource id within the changes

* [Plannable Import] Implement streamed logs for -json plan

* use latest structs

* remove implementation plans from TODO
This commit is contained in:
Liam Cervante 2023-05-04 10:02:06 +02:00 committed by GitHub
parent 54c1c1162f
commit 81eb73731d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 210 additions and 4 deletions

View File

@ -1,7 +1,7 @@
{"@level":"info","@message":"Terraform 0.15.0-dev","@module":"terraform.ui","terraform":"0.15.0-dev","type":"version","ui":"0.1.0"}
{"@level":"info","@message":"test_instance.foo: Plan to create","@module":"terraform.ui","change":{"resource":{"addr":"test_instance.foo","module":"","resource":"test_instance.foo","implied_provider":"test","resource_type":"test_instance","resource_name":"foo","resource_key":null},"action":"create"},"type":"planned_change"}
{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"}
{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","changes":{"add":1,"import":0,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"}
{"@level":"info","@message":"test_instance.foo: Creating...","@module":"terraform.ui","hook":{"resource":{"addr":"test_instance.foo","module":"","resource":"test_instance.foo","implied_provider":"test","resource_type":"test_instance","resource_name":"foo","resource_key":null},"action":"create"},"type":"apply_start"}
{"@level":"info","@message":"test_instance.foo: Creation complete after 0s","@module":"terraform.ui","hook":{"resource":{"addr":"test_instance.foo","module":"","resource":"test_instance.foo","implied_provider":"test","resource_type":"test_instance","resource_name":"foo","resource_key":null},"action":"create","elapsed_seconds":0},"type":"apply_complete"}
{"@level":"info","@message":"Apply complete! Resources: 1 added, 0 changed, 0 destroyed.","@module":"terraform.ui","changes":{"add":1,"change":0,"remove":0,"operation":"apply"},"type":"change_summary"}
{"@level":"info","@message":"Apply complete! Resources: 1 added, 0 changed, 0 destroyed.","@module":"terraform.ui","changes":{"add":1,"import":0,"change":0,"remove":0,"operation":"apply"},"type":"change_summary"}
{"@level":"info","@message":"Outputs: 0","@module":"terraform.ui","outputs":{},"type":"outputs"}

View File

@ -2,4 +2,4 @@
{"@level":"info","@message":"data.test_data_source.a: Refreshing...","@module":"terraform.ui","hook":{"resource":{"addr":"data.test_data_source.a","module":"","resource":"data.test_data_source.a","implied_provider":"test","resource_type":"test_data_source","resource_name":"a","resource_key":null},"action":"read"},"type":"apply_start"}
{"@level":"info","@message":"data.test_data_source.a: Refresh complete after 0s [id=zzzzz]","@module":"terraform.ui","hook":{"resource":{"addr":"data.test_data_source.a","module":"","resource":"data.test_data_source.a","implied_provider":"test","resource_type":"test_data_source","resource_name":"a","resource_key":null},"action":"read","id_key":"id","id_value":"zzzzz","elapsed_seconds":0},"type":"apply_complete"}
{"@level":"info","@message":"test_instance.foo: Plan to create","@module":"terraform.ui","change":{"resource":{"addr":"test_instance.foo","module":"","resource":"test_instance.foo","implied_provider":"test","resource_type":"test_instance","resource_name":"foo","resource_key":null},"action":"create"},"type":"planned_change"}
{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"}
{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","changes":{"add":1,"import":0,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"}

View File

@ -15,6 +15,19 @@ func NewResourceInstanceChange(change *plans.ResourceInstanceChangeSrc) *Resourc
Action: changeAction(change.Action),
Reason: changeReason(change.ActionReason),
}
// The order here matters, we want the moved action to take precedence over
// the import action. We're basically taking "the most recent action" as the
// primary action in the streamed logs. That is to say, that if a resource
// is imported and then moved in a single operation then the change for that
// resource will be reported as ActionMove while the Importing flag will
// still be set to true.
//
// Since both the moved and imported actions only overwrite a NoOp this
// behaviour is consistent across the other actions as well. Something that
// is imported and then updated, or moved and then updated, will have the
// ActionUpdate as the recognised action for the change.
if !change.Addr.Equal(change.PrevRunAddr) {
if c.Action == ActionNoOp {
c.Action = ActionMove
@ -22,6 +35,12 @@ func NewResourceInstanceChange(change *plans.ResourceInstanceChangeSrc) *Resourc
pr := newResourceAddr(change.PrevRunAddr)
c.PreviousResource = &pr
}
if change.Importing != nil {
if c.Action == ActionNoOp {
c.Action = ActionImport
}
c.Importing = &Importing{ID: change.Importing.ID}
}
return c
}
@ -31,6 +50,7 @@ type ResourceInstanceChange struct {
PreviousResource *ResourceAddr `json:"previous_resource,omitempty"`
Action ChangeAction `json:"action"`
Reason ChangeReason `json:"reason,omitempty"`
Importing *Importing `json:"importing,omitempty"`
}
func (c *ResourceInstanceChange) String() string {
@ -47,6 +67,7 @@ const (
ActionUpdate ChangeAction = "update"
ActionReplace ChangeAction = "replace"
ActionDelete ChangeAction = "delete"
ActionImport ChangeAction = "import"
)
func changeAction(action plans.Action) ChangeAction {

View File

@ -16,6 +16,7 @@ const (
type ChangeSummary struct {
Add int `json:"add"`
Change int `json:"change"`
Import int `json:"import"`
Remove int `json:"remove"`
Operation Operation `json:"operation"`
}
@ -24,12 +25,25 @@ type ChangeSummary struct {
// used by Terraform Cloud and Terraform Enterprise, so the exact formats of
// these strings are important.
func (cs *ChangeSummary) String() string {
// TODO(liamcervante): For now, we only include the import count in the plan
// output. This is because counting the imports during the apply is tricky
// and we need to use the actual implementation which isn't ready yet.
//
// We should absolutely fix this before we launch to alpha, but we can't
// do it right now. So we have implemented as much as we can (the plan)
// and will revisit this alongside the concrete implementation of the
// Terraform graph.
switch cs.Operation {
case OperationApplied:
return fmt.Sprintf("Apply complete! Resources: %d added, %d changed, %d destroyed.", cs.Add, cs.Change, cs.Remove)
case OperationDestroyed:
return fmt.Sprintf("Destroy complete! Resources: %d destroyed.", cs.Remove)
case OperationPlanned:
if cs.Import > 0 {
return fmt.Sprintf("Plan: %d to import, %d to add, %d to change, %d to destroy.", cs.Import, cs.Add, cs.Change, cs.Remove)
}
return fmt.Sprintf("Plan: %d to add, %d to change, %d to destroy.", cs.Add, cs.Change, cs.Remove)
default:
return fmt.Sprintf("%s: %d add, %d change, %d destroy", cs.Operation, cs.Add, cs.Change, cs.Remove)

View File

@ -0,0 +1,13 @@
package json
// Importing contains metadata about a resource change that includes an import
// action.
//
// Every field in here should be treated as optional as future versions do not
// make a guarantee that they will retain the format of this change.
//
// Consumers should be capable of rendering/parsing the Importing struct even
// if it does not have the ID field set.
type Importing struct {
ID string `json:"id,omitempty"`
}

View File

@ -205,6 +205,7 @@ func TestJSONView_ChangeSummary(t *testing.T) {
"type": "change_summary",
"changes": map[string]interface{}{
"add": float64(1),
"import": float64(0),
"change": float64(2),
"remove": float64(3),
"operation": "apply",

View File

@ -215,6 +215,11 @@ func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
// Avoid rendering data sources on deletion
continue
}
if change.Importing != nil {
cs.Import++
}
switch change.Action {
case plans.Create:
cs.Add++
@ -227,7 +232,7 @@ func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
cs.Remove++
}
if change.Action != plans.NoOp || !change.Addr.Equal(change.PrevRunAddr) {
if change.Action != plans.NoOp || !change.Addr.Equal(change.PrevRunAddr) || change.Importing != nil {
v.view.PlannedChange(json.NewResourceInstanceChange(change))
}
}

View File

@ -576,6 +576,7 @@ func TestOperationJSON_planNoChanges(t *testing.T) {
"changes": map[string]interface{}{
"operation": "plan",
"add": float64(0),
"import": float64(0),
"change": float64(0),
"remove": float64(0),
},
@ -743,6 +744,7 @@ func TestOperationJSON_plan(t *testing.T) {
"changes": map[string]interface{}{
"operation": "plan",
"add": float64(3),
"import": float64(0),
"change": float64(1),
"remove": float64(3),
},
@ -752,6 +754,153 @@ func TestOperationJSON_plan(t *testing.T) {
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}
func TestOperationJSON_planWithImport(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
v := &OperationJSON{view: NewJSONView(NewView(streams))}
root := addrs.RootModuleInstance
vpc, diags := addrs.ParseModuleInstanceStr("module.vpc")
if len(diags) > 0 {
t.Fatal(diags.Err())
}
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"}
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"}
plan := &plans.Plan{
Changes: &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{
{
Addr: boop.Instance(addrs.IntKey(0)).Absolute(vpc),
PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(vpc),
ChangeSrc: plans.ChangeSrc{Action: plans.NoOp, Importing: &plans.ImportingSrc{ID: "DECD6D77"}},
},
{
Addr: boop.Instance(addrs.IntKey(1)).Absolute(vpc),
PrevRunAddr: boop.Instance(addrs.IntKey(1)).Absolute(vpc),
ChangeSrc: plans.ChangeSrc{Action: plans.Delete, Importing: &plans.ImportingSrc{ID: "DECD6D77"}},
},
{
Addr: boop.Instance(addrs.IntKey(0)).Absolute(root),
PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(root),
ChangeSrc: plans.ChangeSrc{Action: plans.CreateThenDelete, Importing: &plans.ImportingSrc{ID: "DECD6D77"}},
},
{
Addr: beep.Instance(addrs.NoKey).Absolute(root),
PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root),
ChangeSrc: plans.ChangeSrc{Action: plans.Update, Importing: &plans.ImportingSrc{ID: "DECD6D77"}},
},
},
},
}
v.Plan(plan, testSchemas())
want := []map[string]interface{}{
// Simple import
{
"@level": "info",
"@message": "module.vpc.test_resource.boop[0]: Plan to import",
"@module": "terraform.ui",
"type": "planned_change",
"change": map[string]interface{}{
"action": "import",
"resource": map[string]interface{}{
"addr": `module.vpc.test_resource.boop[0]`,
"implied_provider": "test",
"module": "module.vpc",
"resource": `test_resource.boop[0]`,
"resource_key": float64(0),
"resource_name": "boop",
"resource_type": "test_resource",
},
"importing": map[string]interface{}{
"id": "DECD6D77",
},
},
},
// Delete after importing
{
"@level": "info",
"@message": "module.vpc.test_resource.boop[1]: Plan to delete",
"@module": "terraform.ui",
"type": "planned_change",
"change": map[string]interface{}{
"action": "delete",
"resource": map[string]interface{}{
"addr": `module.vpc.test_resource.boop[1]`,
"implied_provider": "test",
"module": "module.vpc",
"resource": `test_resource.boop[1]`,
"resource_key": float64(1),
"resource_name": "boop",
"resource_type": "test_resource",
},
"importing": map[string]interface{}{
"id": "DECD6D77",
},
},
},
// Create-then-delete after importing.
{
"@level": "info",
"@message": "test_resource.boop[0]: Plan to replace",
"@module": "terraform.ui",
"type": "planned_change",
"change": map[string]interface{}{
"action": "replace",
"resource": map[string]interface{}{
"addr": `test_resource.boop[0]`,
"implied_provider": "test",
"module": "",
"resource": `test_resource.boop[0]`,
"resource_key": float64(0),
"resource_name": "boop",
"resource_type": "test_resource",
},
"importing": map[string]interface{}{
"id": "DECD6D77",
},
},
},
// Update after importing
{
"@level": "info",
"@message": "test_resource.beep: Plan to update",
"@module": "terraform.ui",
"type": "planned_change",
"change": map[string]interface{}{
"action": "update",
"resource": map[string]interface{}{
"addr": `test_resource.beep`,
"implied_provider": "test",
"module": "",
"resource": `test_resource.beep`,
"resource_key": nil,
"resource_name": "beep",
"resource_type": "test_resource",
},
"importing": map[string]interface{}{
"id": "DECD6D77",
},
},
},
{
"@level": "info",
"@message": "Plan: 4 to import, 1 to add, 1 to change, 2 to destroy.",
"@module": "terraform.ui",
"type": "change_summary",
"changes": map[string]interface{}{
"operation": "plan",
"add": float64(1),
"import": float64(4),
"change": float64(1),
"remove": float64(2),
},
},
}
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}
func TestOperationJSON_planDriftWithMove(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
v := &OperationJSON{view: NewJSONView(NewView(streams))}
@ -879,6 +1028,7 @@ func TestOperationJSON_planDriftWithMove(t *testing.T) {
"changes": map[string]interface{}{
"operation": "plan",
"add": float64(0),
"import": float64(0),
"change": float64(0),
"remove": float64(0),
},
@ -1009,6 +1159,7 @@ func TestOperationJSON_planDriftWithMoveRefreshOnly(t *testing.T) {
"changes": map[string]interface{}{
"operation": "plan",
"add": float64(0),
"import": float64(0),
"change": float64(0),
"remove": float64(0),
},
@ -1068,6 +1219,7 @@ func TestOperationJSON_planOutputChanges(t *testing.T) {
"changes": map[string]interface{}{
"operation": "plan",
"add": float64(0),
"import": float64(0),
"change": float64(0),
"remove": float64(0),
},