command/show (json): marshal the state snapshot included with the plan file (#21597)

* command/show: marshal the state snapshot from the planfile

The planfile contains a state snapshot with certain resources updated
(outputs and datasources). Previously `terraform show -json PLANFILE`
was using the current state instead of the state inside the plan as
intended.

This caused an issue when the state included a terraform_remote_state
datasource. The datasource's state gets refreshed - and therefore
upgraded to the current state version - during plan, but that won't
persist to state until apply.

* update comment to reflect new return
This commit is contained in:
Kristin Laemmert 2019-06-05 07:29:02 -04:00 committed by GitHub
parent 2f893f2b95
commit b9f114aa25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 55 additions and 43 deletions

View File

@ -91,12 +91,7 @@ func Marshal(
p *plans.Plan, p *plans.Plan,
sf *statefile.File, sf *statefile.File,
schemas *terraform.Schemas, schemas *terraform.Schemas,
stateSchemas *terraform.Schemas,
) ([]byte, error) { ) ([]byte, error) {
if stateSchemas == nil {
stateSchemas = schemas
}
output := newPlan() output := newPlan()
output.TerraformVersion = version.String() output.TerraformVersion = version.String()
@ -125,7 +120,7 @@ func Marshal(
// output.PriorState // output.PriorState
if sf != nil && !sf.State.Empty() { if sf != nil && !sf.State.Empty() {
output.PriorState, err = jsonstate.Marshal(sf, stateSchemas) output.PriorState, err = jsonstate.Marshal(sf, schemas)
if err != nil { if err != nil {
return nil, fmt.Errorf("error marshaling prior state: %s", err) return nil, fmt.Errorf("error marshaling prior state: %s", err)
} }

View File

@ -115,7 +115,7 @@ func (c *ShowCommand) Run(args []string) int {
// if that fails, try to read the cli argument as a path to a statefile // if that fails, try to read the cli argument as a path to a statefile
if len(args) > 0 { if len(args) > 0 {
path := args[0] path := args[0]
plan, planErr = getPlanFromPath(path) plan, stateFile, planErr = getPlanFromPath(path)
if planErr != nil { if planErr != nil {
stateFile, stateErr = getStateFromPath(path) stateFile, stateErr = getStateFromPath(path)
if stateErr != nil { if stateErr != nil {
@ -129,9 +129,7 @@ func (c *ShowCommand) Run(args []string) int {
return 1 return 1
} }
} }
} } else {
if stateFile == nil {
env := c.Workspace() env := c.Workspace()
stateFile, stateErr = getStateFromEnv(b, env) stateFile, stateErr = getStateFromEnv(b, env)
if err != nil { if err != nil {
@ -143,29 +141,7 @@ func (c *ShowCommand) Run(args []string) int {
if plan != nil { if plan != nil {
if jsonOutput == true { if jsonOutput == true {
config := ctx.Config() config := ctx.Config()
jsonPlan, err := jsonplan.Marshal(config, plan, stateFile, schemas)
var err error
var jsonPlan []byte
// If there is no prior state, we have all the schemas needed.
if stateFile == nil {
jsonPlan, err = jsonplan.Marshal(config, plan, stateFile, schemas, nil)
} else {
// If there is state, we need the state-specific schemas, which
// may differ from the schemas loaded from the plan.
// This occurs if there is a data_source in the state that was
// removed from the configuration, because terraform core does
// not need to load the schema to remove a data source.
opReq.PlanFile = nil
ctx, _, ctxDiags := local.Context(opReq)
diags = diags.Append(ctxDiags)
if ctxDiags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
stateSchemas := ctx.Schemas()
jsonPlan, err = jsonplan.Marshal(config, plan, stateFile, schemas, stateSchemas)
}
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to marshal plan to json: %s", err)) c.Ui.Error(fmt.Sprintf("Failed to marshal plan to json: %s", err))
@ -224,19 +200,21 @@ func (c *ShowCommand) Synopsis() string {
return "Inspect Terraform state or plan" return "Inspect Terraform state or plan"
} }
// getPlanFromPath returns a plan if the user-supplied path points to a planfile. // getPlanFromPath returns a plan and statefile if the user-supplied path points
// If both plan and error are nil, the path is likely a directory. // to a planfile. If both plan and error are nil, the path is likely a
// An error could suggest that the given path points to a statefile. // directory. An error could suggest that the given path points to a statefile.
func getPlanFromPath(path string) (*plans.Plan, error) { func getPlanFromPath(path string) (*plans.Plan, *statefile.File, error) {
pr, err := planfile.Open(path) pr, err := planfile.Open(path)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
plan, err := pr.ReadPlan() plan, err := pr.ReadPlan()
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
return plan, nil
stateFile, err := pr.ReadStateFile()
return plan, stateFile, nil
} }
// getStateFromPath returns a statefile if the user-supplied path points to a statefile. // getStateFromPath returns a statefile if the user-supplied path points to a statefile.

View File

@ -412,6 +412,11 @@ type plan struct {
PlannedValues map[string]interface{} `json:"planned_values,omitempty"` PlannedValues map[string]interface{} `json:"planned_values,omitempty"`
ResourceChanges []interface{} `json:"resource_changes,omitempty"` ResourceChanges []interface{} `json:"resource_changes,omitempty"`
OutputChanges map[string]interface{} `json:"output_changes,omitempty"` OutputChanges map[string]interface{} `json:"output_changes,omitempty"`
PriorState map[string]interface{} `json:"prior_state,omitempty"` PriorState priorState `json:"prior_state,omitempty"`
Config map[string]interface{} `json:"configuration,omitempty"` Config map[string]interface{} `json:"configuration,omitempty"`
} }
type priorState struct {
FormatVersion string `json:"format_version,omitempty"`
Values map[string]interface{} `json:"values,omitempty"`
}

View File

@ -53,6 +53,18 @@
] ]
} }
}, },
"prior_state": {
"format_version": "0.1",
"values": {
"outputs": {
"test": {
"sensitive": false,
"value": "bar"
}
},
"root_module": {}
}
},
"resource_changes": [ "resource_changes": [
{ {
"address": "test_instance.test[0]", "address": "test_instance.test[0]",

View File

@ -82,8 +82,13 @@
}, },
"prior_state": { "prior_state": {
"format_version": "0.1", "format_version": "0.1",
"terraform_version": "0.12.0",
"values": { "values": {
"outputs": {
"test": {
"sensitive": false,
"value": "bar"
}
},
"root_module": { "root_module": {
"resources": [ "resources": [
{ {

View File

@ -64,8 +64,13 @@
}, },
"prior_state": { "prior_state": {
"format_version": "0.1", "format_version": "0.1",
"terraform_version": "0.12.0",
"values": { "values": {
"outputs": {
"test": {
"sensitive": false,
"value": "bar"
}
},
"root_module": { "root_module": {
"resources": [ "resources": [
{ {

View File

@ -69,6 +69,18 @@
] ]
} }
}, },
"prior_state": {
"format_version": "0.1",
"values": {
"outputs": {
"test": {
"sensitive": false,
"value": "baz"
}
},
"root_module": {}
}
},
"resource_changes": [ "resource_changes": [
{ {
"address": "module.module_test_bar.test_instance.test", "address": "module.module_test_bar.test_instance.test",