302 lines
8.5 KiB
Go
302 lines
8.5 KiB
Go
package jsonplan
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
ctyjson "github.com/zclconf/go-cty/cty/json"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/command/jsonstate"
|
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform/internal/plans"
|
|
"github.com/hashicorp/terraform/internal/states"
|
|
"github.com/hashicorp/terraform/internal/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 {
|
|
if value == cty.NilVal || value.IsNull() {
|
|
return nil
|
|
}
|
|
ret := make(attributeValues)
|
|
|
|
it := value.ElementIterator()
|
|
for it.Next() {
|
|
k, v := it.Element()
|
|
vJSON, _ := ctyjson.Marshal(v, v.Type())
|
|
ret[k.AsString()] = json.RawMessage(vJSON)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// marshalPlannedOutputs takes a list of changes and returns a map of output
|
|
// values
|
|
func marshalPlannedOutputs(changes *plans.Changes) (map[string]output, error) {
|
|
if changes.Outputs == nil {
|
|
// No changes - we're done here!
|
|
return nil, nil
|
|
}
|
|
|
|
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 {
|
|
return ret, err
|
|
}
|
|
// The values may be marked, but we must rely on the Sensitive flag
|
|
// as the decoded value is only an intermediate step in transcoding
|
|
// this to a json format.
|
|
changeV.After, _ = changeV.After.UnmarkDeep()
|
|
|
|
if changeV.After != cty.NilVal && changeV.After.IsWhollyKnown() {
|
|
after, err = ctyjson.Marshal(changeV.After, changeV.After.Type())
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
}
|
|
|
|
ret[oc.Addr.OutputValue.Name] = output{
|
|
Value: json.RawMessage(after),
|
|
Sensitive: oc.Sensitive,
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
|
|
}
|
|
|
|
func marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) (module, error) {
|
|
var ret module
|
|
|
|
// build two maps:
|
|
// module name -> [resource addresses]
|
|
// module -> [children modules]
|
|
moduleResourceMap := make(map[string][]addrs.AbsResourceInstance)
|
|
moduleMap := make(map[string][]addrs.ModuleInstance)
|
|
seenModules := make(map[string]bool)
|
|
|
|
for _, resource := range changes.Resources {
|
|
// If the resource is being deleted, skip over it.
|
|
// Deposed instances are always conceptually a destroy, but if they
|
|
// were gone during refresh then the change becomes a noop.
|
|
if resource.Action != plans.Delete && resource.DeposedKey == states.NotDeposed {
|
|
containingModule := resource.Addr.Module.String()
|
|
moduleResourceMap[containingModule] = append(moduleResourceMap[containingModule], resource.Addr)
|
|
|
|
// the root module has no parents
|
|
if !resource.Addr.Module.IsRoot() {
|
|
parent := resource.Addr.Module.Parent().String()
|
|
// we expect to see multiple resources in one module, so we
|
|
// only need to report the "parent" module for each child module
|
|
// once.
|
|
if !seenModules[containingModule] {
|
|
moduleMap[parent] = append(moduleMap[parent], resource.Addr.Module)
|
|
seenModules[containingModule] = true
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// start with the root module
|
|
resources, err := marshalPlanResources(changes, moduleResourceMap[""], schemas)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
ret.Resources = resources
|
|
|
|
childModules, err := marshalPlanModules(changes, schemas, moduleMap[""], moduleMap, moduleResourceMap)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
sort.Slice(childModules, func(i, j int) bool {
|
|
return childModules[i].Address < childModules[j].Address
|
|
})
|
|
|
|
ret.ChildModules = childModules
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// marshalPlanResources
|
|
func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstance, schemas *terraform.Schemas) ([]resource, error) {
|
|
var ret []resource
|
|
|
|
for _, ri := range ris {
|
|
r := changes.ResourceInstance(ri)
|
|
if r.Action == plans.Delete {
|
|
continue
|
|
}
|
|
|
|
resource := resource{
|
|
Address: r.Addr.String(),
|
|
Type: r.Addr.Resource.Resource.Type,
|
|
Name: r.Addr.Resource.Resource.Name,
|
|
ProviderName: r.ProviderAddr.Provider.String(),
|
|
Index: r.Addr.Resource.Key,
|
|
}
|
|
|
|
switch r.Addr.Resource.Resource.Mode {
|
|
case addrs.ManagedResourceMode:
|
|
resource.Mode = "managed"
|
|
case addrs.DataResourceMode:
|
|
resource.Mode = "data"
|
|
default:
|
|
return nil, fmt.Errorf("resource %s has an unsupported mode %s",
|
|
r.Addr.String(),
|
|
r.Addr.Resource.Resource.Mode.String(),
|
|
)
|
|
}
|
|
|
|
schema, schemaVer := schemas.ResourceTypeConfig(
|
|
r.ProviderAddr.Provider,
|
|
r.Addr.Resource.Resource.Mode,
|
|
resource.Type,
|
|
)
|
|
if schema == nil {
|
|
return nil, fmt.Errorf("no schema found for %s", r.Addr.String())
|
|
}
|
|
resource.SchemaVersion = schemaVer
|
|
changeV, err := r.Decode(schema.ImpliedType())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// copy the marked After values so we can use these in marshalSensitiveValues
|
|
markedAfter := changeV.After
|
|
|
|
// The values may be marked, but we must rely on the Sensitive flag
|
|
// as the decoded value is only an intermediate step in transcoding
|
|
// this to a json format.
|
|
changeV.Before, _ = changeV.Before.UnmarkDeep()
|
|
changeV.After, _ = changeV.After.UnmarkDeep()
|
|
|
|
if changeV.After != cty.NilVal {
|
|
if changeV.After.IsWhollyKnown() {
|
|
resource.AttributeValues = marshalAttributeValues(changeV.After, schema)
|
|
} else {
|
|
knowns := omitUnknowns(changeV.After)
|
|
resource.AttributeValues = marshalAttributeValues(knowns, schema)
|
|
}
|
|
}
|
|
|
|
s := jsonstate.SensitiveAsBool(markedAfter)
|
|
v, err := ctyjson.Marshal(s, s.Type())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resource.SensitiveValues = v
|
|
|
|
ret = append(ret, resource)
|
|
}
|
|
|
|
sort.Slice(ret, func(i, j int) bool {
|
|
return ret[i].Address < ret[j].Address
|
|
})
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
rs, err := marshalPlanResources(changes, moduleResources, schemas)
|
|
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
|
|
}
|
|
|
|
// marshalSensitiveValues returns a map of sensitive attributes, with the value
|
|
// set to true. It returns nil if the value is nil or if there are no sensitive
|
|
// vals.
|
|
func marshalSensitiveValues(value cty.Value) map[string]bool {
|
|
if value.RawEquals(cty.NilVal) || value.IsNull() {
|
|
return nil
|
|
}
|
|
|
|
ret := make(map[string]bool)
|
|
|
|
it := value.ElementIterator()
|
|
for it.Next() {
|
|
k, v := it.Element()
|
|
s := jsonstate.SensitiveAsBool(v)
|
|
if !s.RawEquals(cty.False) {
|
|
ret[k.AsString()] = true
|
|
}
|
|
}
|
|
|
|
if len(ret) == 0 {
|
|
return nil
|
|
}
|
|
return ret
|
|
}
|