mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
[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:
parent
54c1c1162f
commit
81eb73731d
@ -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"}
|
||||
|
@ -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"}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
13
internal/command/views/json/importing.go
Normal file
13
internal/command/views/json/importing.go
Normal 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"`
|
||||
}
|
@ -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",
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user