2018-12-19 20:08:25 +01:00
|
|
|
package jsonplan
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2019-01-25 18:17:40 +01:00
|
|
|
"sort"
|
2018-12-19 20:08:25 +01:00
|
|
|
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
2019-01-25 18:17:40 +01:00
|
|
|
ctyjson "github.com/zclconf/go-cty/cty/json"
|
2018-12-19 20:08:25 +01:00
|
|
|
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
|
|
"github.com/hashicorp/terraform/configs/configschema"
|
|
|
|
"github.com/hashicorp/terraform/plans"
|
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
|
|
)
|
|
|
|
|
|
|
|
// stateValues is the common representation of resolved values for both the
|
|
|
|
// prior state (which is always complete) and the planned new state.
|
|
|
|
type stateValues struct {
|
|
|
|
Outputs map[string]output `json:"outputs,omitempty"`
|
|
|
|
RootModule module `json:"root_module,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// attributeValues is the JSON representation of the attribute values of the
|
|
|
|
// resource, whose structure depends on the resource type schema.
|
|
|
|
type attributeValues map[string]interface{}
|
|
|
|
|
|
|
|
func marshalAttributeValues(value cty.Value, schema *configschema.Block) attributeValues {
|
2019-11-25 21:01:38 +01:00
|
|
|
if value == cty.NilVal || value.IsNull() {
|
2019-01-09 17:59:11 +01:00
|
|
|
return nil
|
|
|
|
}
|
2018-12-19 20:08:25 +01:00
|
|
|
ret := make(attributeValues)
|
|
|
|
|
|
|
|
it := value.ElementIterator()
|
|
|
|
for it.Next() {
|
|
|
|
k, v := it.Element()
|
mildwonkey/b-show-state (#20032)
* command/show: properly marshal attribute values to json
marshalAttributeValues in jsonstate and jsonplan packages was returning
a cty.Value, which json/encoding could not marshal. These functions now
convert those cty.Values into json.RawMessages.
* command/jsonplan: planned values should include resources that are not changing
* command/jsonplan: return a filtered list of proposed 'after' attributes
Previously, proposed 'after' attributes were not being shown if the
attributes were not WhollyKnown. jsonplan now iterates through all the
`after` attributes, omitting those which are not wholly known.
The same was roughly true for after_unknown, and that structure is now
correctly populated. In the future we may choose to filter the
after_unknown structure to _only_ display unknown attributes, instead of
all attributes.
* command/jsonconfig: use a unique key for providers so that aliased
providers don't get munged together
This now uses the same "provider" key from configs.Module, e.g.
`providername.provideralias`.
* command/jsonplan: unknownAsBool needs to iterate through objects that are not wholly known
* command/jsonplan: properly display actions as strings according to the RFC,
instead of a plans.Action string.
For example:
a plans.Action string DeleteThenCreate should be displayed as ["delete",
"create"]
Tests have been updated to reflect this.
* command/jsonplan: return "null" for unknown list items.
The length of a list could be meaningful on its own, so we will turn
unknowns into "null". The same is less likely true for maps and objects,
so we will continue to omit unknown values from those.
2019-01-23 20:46:53 +01:00
|
|
|
vJSON, _ := ctyjson.Marshal(v, v.Type())
|
|
|
|
ret[k.AsString()] = json.RawMessage(vJSON)
|
2018-12-19 20:08:25 +01:00
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
2019-01-12 00:13:55 +01:00
|
|
|
// marshalPlannedOutputs takes a list of changes and returns a map of output
|
|
|
|
// values
|
2018-12-20 23:30:18 +01:00
|
|
|
func marshalPlannedOutputs(changes *plans.Changes) (map[string]output, error) {
|
2018-12-19 20:08:25 +01:00
|
|
|
if changes.Outputs == nil {
|
|
|
|
// No changes - we're done here!
|
2018-12-20 23:30:18 +01:00
|
|
|
return nil, nil
|
2018-12-19 20:08:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
ret := make(map[string]output)
|
|
|
|
|
|
|
|
for _, oc := range changes.Outputs {
|
|
|
|
if oc.ChangeSrc.Action == plans.Delete {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
var after []byte
|
|
|
|
changeV, err := oc.Decode()
|
|
|
|
if err != nil {
|
2018-12-20 23:30:18 +01:00
|
|
|
return ret, err
|
2018-12-19 20:08:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if changeV.After != cty.NilVal {
|
|
|
|
if changeV.After.IsWhollyKnown() {
|
|
|
|
after, err = ctyjson.Marshal(changeV.After, changeV.After.Type())
|
|
|
|
if err != nil {
|
2018-12-20 23:30:18 +01:00
|
|
|
return ret, err
|
2018-12-19 20:08:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ret[oc.Addr.OutputValue.Name] = output{
|
|
|
|
Value: json.RawMessage(after),
|
|
|
|
Sensitive: oc.Sensitive,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-20 23:30:18 +01:00
|
|
|
return ret, nil
|
2018-12-19 20:08:25 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-12-20 23:30:18 +01:00
|
|
|
func marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) (module, error) {
|
|
|
|
var ret module
|
2018-12-19 20:08:25 +01:00
|
|
|
|
|
|
|
// build two maps:
|
|
|
|
// module name -> [resource addresses]
|
|
|
|
// module -> [children modules]
|
|
|
|
moduleResourceMap := make(map[string][]addrs.AbsResourceInstance)
|
|
|
|
moduleMap := make(map[string][]addrs.ModuleInstance)
|
2019-02-19 17:12:33 +01:00
|
|
|
seenModules := make(map[string]bool)
|
2018-12-19 20:08:25 +01:00
|
|
|
|
|
|
|
for _, resource := range changes.Resources {
|
|
|
|
// if the resource is being deleted, skip over it.
|
|
|
|
if resource.Action != plans.Delete {
|
|
|
|
containingModule := resource.Addr.Module.String()
|
|
|
|
moduleResourceMap[containingModule] = append(moduleResourceMap[containingModule], resource.Addr)
|
|
|
|
|
2019-10-17 17:33:04 +02:00
|
|
|
// the root module has no parents
|
|
|
|
if !resource.Addr.Module.IsRoot() {
|
2018-12-19 20:08:25 +01:00
|
|
|
parent := resource.Addr.Module.Parent().String()
|
2019-10-17 17:33:04 +02:00
|
|
|
// we expect to see multiple resources in one module, so we
|
2019-03-01 22:59:12 +01:00
|
|
|
// only need to report the "parent" module for each child module
|
|
|
|
// once.
|
|
|
|
if !seenModules[containingModule] {
|
2019-02-19 17:12:33 +01:00
|
|
|
moduleMap[parent] = append(moduleMap[parent], resource.Addr.Module)
|
2019-03-01 22:59:12 +01:00
|
|
|
seenModules[containingModule] = true
|
2019-02-19 17:12:33 +01:00
|
|
|
}
|
2019-10-17 17:33:04 +02:00
|
|
|
|
|
|
|
// If any given parent module has no resources, it needs to be
|
|
|
|
// added to the moduleMap. This walks through the current
|
|
|
|
// resources' modules' ancestors, taking advantage of the fact
|
|
|
|
// that Ancestors() returns an ordered slice, and verifies that
|
|
|
|
// each one is in the map.
|
|
|
|
ancestors := resource.Addr.Module.Ancestors()
|
|
|
|
for i, ancestor := range ancestors[:len(ancestors)-1] {
|
|
|
|
aStr := ancestor.String()
|
|
|
|
|
|
|
|
// childStr here is the immediate child of the current step
|
|
|
|
childStr := ancestors[i+1].String()
|
|
|
|
// we likely will see multiple resources in one module, so we
|
|
|
|
// only need to report the "parent" module for each child module
|
|
|
|
// once.
|
|
|
|
if !seenModules[childStr] {
|
|
|
|
moduleMap[aStr] = append(moduleMap[aStr], ancestors[i+1])
|
|
|
|
seenModules[childStr] = true
|
|
|
|
}
|
|
|
|
}
|
2018-12-19 20:08:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// start with the root module
|
2018-12-20 23:30:18 +01:00
|
|
|
resources, err := marshalPlanResources(changes, moduleResourceMap[""], schemas)
|
2018-12-19 20:08:25 +01:00
|
|
|
if err != nil {
|
2018-12-20 23:30:18 +01:00
|
|
|
return ret, err
|
2018-12-19 20:08:25 +01:00
|
|
|
}
|
|
|
|
ret.Resources = resources
|
|
|
|
|
|
|
|
childModules, err := marshalPlanModules(changes, schemas, moduleMap[""], moduleMap, moduleResourceMap)
|
|
|
|
if err != nil {
|
2018-12-20 23:30:18 +01:00
|
|
|
return ret, err
|
2018-12-19 20:08:25 +01:00
|
|
|
}
|
2019-03-01 22:59:12 +01:00
|
|
|
sort.Slice(childModules, func(i, j int) bool {
|
|
|
|
return childModules[i].Address < childModules[j].Address
|
|
|
|
})
|
|
|
|
|
2018-12-19 20:08:25 +01:00
|
|
|
ret.ChildModules = childModules
|
|
|
|
|
2018-12-20 23:30:18 +01:00
|
|
|
return ret, nil
|
2018-12-19 20:08:25 +01:00
|
|
|
}
|
|
|
|
|
2019-01-09 17:59:11 +01:00
|
|
|
// marshalPlanResources
|
2018-12-20 23:30:18 +01:00
|
|
|
func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstance, schemas *terraform.Schemas) ([]resource, error) {
|
|
|
|
var ret []resource
|
2018-12-19 20:08:25 +01:00
|
|
|
|
|
|
|
for _, ri := range ris {
|
|
|
|
r := changes.ResourceInstance(ri)
|
mildwonkey/b-show-state (#20032)
* command/show: properly marshal attribute values to json
marshalAttributeValues in jsonstate and jsonplan packages was returning
a cty.Value, which json/encoding could not marshal. These functions now
convert those cty.Values into json.RawMessages.
* command/jsonplan: planned values should include resources that are not changing
* command/jsonplan: return a filtered list of proposed 'after' attributes
Previously, proposed 'after' attributes were not being shown if the
attributes were not WhollyKnown. jsonplan now iterates through all the
`after` attributes, omitting those which are not wholly known.
The same was roughly true for after_unknown, and that structure is now
correctly populated. In the future we may choose to filter the
after_unknown structure to _only_ display unknown attributes, instead of
all attributes.
* command/jsonconfig: use a unique key for providers so that aliased
providers don't get munged together
This now uses the same "provider" key from configs.Module, e.g.
`providername.provideralias`.
* command/jsonplan: unknownAsBool needs to iterate through objects that are not wholly known
* command/jsonplan: properly display actions as strings according to the RFC,
instead of a plans.Action string.
For example:
a plans.Action string DeleteThenCreate should be displayed as ["delete",
"create"]
Tests have been updated to reflect this.
* command/jsonplan: return "null" for unknown list items.
The length of a list could be meaningful on its own, so we will turn
unknowns into "null". The same is less likely true for maps and objects,
so we will continue to omit unknown values from those.
2019-01-23 20:46:53 +01:00
|
|
|
if r.Action == plans.Delete {
|
2018-12-19 20:08:25 +01:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
resource := resource{
|
|
|
|
Address: r.Addr.String(),
|
|
|
|
Type: r.Addr.Resource.Resource.Type,
|
|
|
|
Name: r.Addr.Resource.Resource.Name,
|
|
|
|
ProviderName: r.ProviderAddr.ProviderConfig.StringCompact(),
|
|
|
|
Index: r.Addr.Resource.Key,
|
|
|
|
}
|
|
|
|
|
|
|
|
switch r.Addr.Resource.Resource.Mode {
|
|
|
|
case addrs.ManagedResourceMode:
|
|
|
|
resource.Mode = "managed"
|
|
|
|
case addrs.DataResourceMode:
|
|
|
|
resource.Mode = "data"
|
|
|
|
default:
|
2018-12-20 23:30:18 +01:00
|
|
|
return nil, fmt.Errorf("resource %s has an unsupported mode %s",
|
2018-12-19 20:08:25 +01:00
|
|
|
r.Addr.String(),
|
|
|
|
r.Addr.Resource.Resource.Mode.String(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
schema, schemaVer := schemas.ResourceTypeConfig(
|
2019-02-20 23:27:49 +01:00
|
|
|
r.ProviderAddr.ProviderConfig.Type,
|
2018-12-19 20:08:25 +01:00
|
|
|
r.Addr.Resource.Resource.Mode,
|
|
|
|
resource.Type,
|
|
|
|
)
|
|
|
|
if schema == nil {
|
2018-12-20 23:30:18 +01:00
|
|
|
return nil, fmt.Errorf("no schema found for %s", r.Addr.String())
|
2018-12-19 20:08:25 +01:00
|
|
|
}
|
|
|
|
resource.SchemaVersion = schemaVer
|
|
|
|
changeV, err := r.Decode(schema.ImpliedType())
|
|
|
|
if err != nil {
|
2018-12-20 23:30:18 +01:00
|
|
|
return nil, err
|
2018-12-19 20:08:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if changeV.After != cty.NilVal {
|
|
|
|
if changeV.After.IsWhollyKnown() {
|
|
|
|
resource.AttributeValues = marshalAttributeValues(changeV.After, schema)
|
2019-02-11 22:17:03 +01:00
|
|
|
} else {
|
|
|
|
knowns := omitUnknowns(changeV.After)
|
|
|
|
resource.AttributeValues = marshalAttributeValues(knowns, schema)
|
2018-12-19 20:08:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ret = append(ret, resource)
|
|
|
|
}
|
|
|
|
|
2019-01-25 18:17:40 +01:00
|
|
|
sort.Slice(ret, func(i, j int) bool {
|
|
|
|
return ret[i].Address < ret[j].Address
|
|
|
|
})
|
|
|
|
|
2018-12-20 23:30:18 +01:00
|
|
|
return ret, nil
|
2018-12-19 20:08:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// marshalPlanModules iterates over a list of modules to recursively describe
|
|
|
|
// the full module tree.
|
|
|
|
func marshalPlanModules(
|
|
|
|
changes *plans.Changes,
|
|
|
|
schemas *terraform.Schemas,
|
|
|
|
childModules []addrs.ModuleInstance,
|
|
|
|
moduleMap map[string][]addrs.ModuleInstance,
|
|
|
|
moduleResourceMap map[string][]addrs.AbsResourceInstance,
|
|
|
|
) ([]module, error) {
|
|
|
|
|
|
|
|
var ret []module
|
|
|
|
|
|
|
|
for _, child := range childModules {
|
|
|
|
moduleResources := moduleResourceMap[child.String()]
|
|
|
|
// cm for child module, naming things is hard.
|
|
|
|
var cm module
|
|
|
|
// don't populate the address for the root module
|
|
|
|
if child.String() != "" {
|
|
|
|
cm.Address = child.String()
|
|
|
|
}
|
2018-12-20 23:30:18 +01:00
|
|
|
rs, err := marshalPlanResources(changes, moduleResources, schemas)
|
2018-12-19 20:08:25 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
cm.Resources = rs
|
|
|
|
|
|
|
|
if len(moduleMap[child.String()]) > 0 {
|
|
|
|
moreChildModules, err := marshalPlanModules(changes, schemas, moduleMap[child.String()], moduleMap, moduleResourceMap)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
cm.ChildModules = moreChildModules
|
|
|
|
}
|
|
|
|
|
|
|
|
ret = append(ret, cm)
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret, nil
|
|
|
|
}
|