From 3e9c51c7268d195c4917fd455742df4fa8fdfe1c Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Thu, 3 Jan 2019 12:08:03 -0800 Subject: [PATCH] command/jsonstate: marshal state into a machine readable blob o'json (#19911) * command/jsonstate: marshal state into a machine readable blob o'json * consistent json tags are nice --- command/jsonplan/plan.go | 2 +- command/jsonstate/doc.go | 3 + command/jsonstate/expression.go | 25 ---- command/jsonstate/state.go | 222 ++++++++++++++++++++++++++++---- 4 files changed, 198 insertions(+), 54 deletions(-) create mode 100644 command/jsonstate/doc.go delete mode 100644 command/jsonstate/expression.go diff --git a/command/jsonplan/plan.go b/command/jsonplan/plan.go index 7f0b68b1c..ff5d95b3b 100644 --- a/command/jsonplan/plan.go +++ b/command/jsonplan/plan.go @@ -100,7 +100,7 @@ func Marshal( } // output.PriorState - output.PriorState, err = jsonstate.Marshal(s) + output.PriorState, err = jsonstate.Marshal(s, schemas) if err != nil { return nil, fmt.Errorf("error marshaling prior state: %s", err) } diff --git a/command/jsonstate/doc.go b/command/jsonstate/doc.go new file mode 100644 index 000000000..54773d09c --- /dev/null +++ b/command/jsonstate/doc.go @@ -0,0 +1,3 @@ +// Package jsonstate implements methods for outputting a state in a +// machine-readable json format +package jsonstate diff --git a/command/jsonstate/expression.go b/command/jsonstate/expression.go deleted file mode 100644 index 4709893f2..000000000 --- a/command/jsonstate/expression.go +++ /dev/null @@ -1,25 +0,0 @@ -package jsonstate - -import "encoding/json" - -// expression represents any unparsed expression -type expression struct { - // "constant_value" is set only if the expression contains no references to - // other objects, in which case it gives the resulting constant value. This - // is mapped as for the individual values in the common value - // representation. - ConstantValue json.RawMessage `json:"constant_value,omitempty"` - - // Alternatively, "references" will be set to a list of references in the - // expression. Multi-step references will be unwrapped and duplicated for - // each significant traversal step, allowing callers to more easily - // recognize the objects they care about without attempting to parse the - // expressions. Callers should only use string equality checks here, since - // the syntax may be extended in future releases. - References []string `json:"references,omitempty"` - - // "source" is an object describing the source span of this expression in - // the configuration. Callers might use this, for example, to extract a raw - // source code snippet for display purposes. - Source source `json:"source"` -} diff --git a/command/jsonstate/state.go b/command/jsonstate/state.go index ee6eac9e9..b36262288 100644 --- a/command/jsonstate/state.go +++ b/command/jsonstate/state.go @@ -2,8 +2,15 @@ package jsonstate import ( "encoding/json" + "fmt" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/terraform" ) // FormatVersion represents the version of the json format and will be @@ -14,26 +21,26 @@ const FormatVersion = "0.1" // state is the top-level representation of the json format of a terraform // state. type state struct { - FormatVersion string `json:"format_version"` - Values stateValues `json:"values"` + FormatVersion string `json:"format_version,omitempty"` + Values stateValues `json:"values,omitempty"` } // 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 - RootModule module + Outputs map[string]output `json:"outputs,omitempty"` + RootModule module `json:"root_module,omitempty"` } type output struct { - Sensitive bool - Value json.RawMessage + Sensitive bool `json:"sensitive,omitempty"` + Value json.RawMessage `json:"value,omitempty"` } // module is the representation of a module in state. This can be the root module // or a child module type module struct { - Resources []resource + Resources []resource `json:"resources,omitempty"` // Address is the absolute module address, omitted for the root module Address string `json:"address,omitempty"` @@ -43,27 +50,19 @@ type module struct { ChildModules []module `json:"child_modules,omitempty"` } -type moduleCall struct { - ResolvedSource string `json:"resolved_source"` - Expressions map[string]interface{} `json:"expressions,omitempty"` - CountExpression expression `json:"count_expression"` - ForEachExpression expression `json:"for_each_expression"` - Module module `json:"module"` -} - // Resource is the representation of a resource in the state. type resource struct { // Address is the absolute resource address - Address string `json:"address"` + Address string `json:"address,omitempty"` // Mode can be "managed" or "data" - Mode string `json:"mode"` + Mode string `json:"mode,omitempty"` - Type string `json:"type"` - Name string `json:"name"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` // Index is omitted for a resource not using `count` or `for_each`. - Index int `json:"index,omitempty"` + Index addrs.InstanceKey `json:"index,omitempty"` // ProviderName allows the property "type" to be interpreted unambiguously // in the unusual situation where a provider offers a resource type whose @@ -73,19 +72,28 @@ type resource struct { // SchemaVersion indicates which version of the resource type schema the // "values" property conforms to. - SchemaVersion int `json:"schema_version"` + SchemaVersion uint64 `json:"schema_version,omitempty"` - // Values is the JSON representation of the attribute values of the + // AttributeValues is the JSON representation of the attribute values of the // resource, whose structure depends on the resource type schema. Any // unknown values are omitted or set to null, making them indistinguishable // from absent values. - Values json.RawMessage `json:"values"` + AttributeValues attributeValues `json:"values,omitempty"` } -type source struct { - FileName string `json:"filename"` - Start string `json:"start"` - End string `json:"end"` +// 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 { + ret := make(attributeValues) + + it := value.ElementIterator() + for it.Next() { + k, v := it.Element() + ret[k.AsString()] = v + } + return ret } // newState() returns a minimally-initialized state @@ -96,13 +104,171 @@ func newState() *state { } // Marshal returns the json encoding of a terraform plan. -func Marshal(s *states.State) ([]byte, error) { +func Marshal(s *states.State, schemas *terraform.Schemas) ([]byte, error) { if s.Empty() { return nil, nil } output := newState() + // output.StateValues + err := output.marshalStateValues(s, schemas) + if err != nil { + return nil, err + } + ret, err := json.Marshal(output) return ret, err } + +func (jsonstate *state) marshalStateValues(s *states.State, schemas *terraform.Schemas) error { + var sv stateValues + var err error + + // only marshal the root module outputs + sv.Outputs, err = marshalOutputs(s.RootModule().OutputValues) + if err != nil { + return err + } + + // use the state and module map to build up the module structure + sv.RootModule, err = marshalRootModule(s, schemas) + if err != nil { + return err + } + + jsonstate.Values = sv + return nil +} + +func marshalOutputs(outputs map[string]*states.OutputValue) (map[string]output, error) { + if outputs == nil { + return nil, nil + } + + ret := make(map[string]output) + for k, v := range outputs { + ov, err := ctyjson.Marshal(v.Value, v.Value.Type()) + if err != nil { + return ret, err + } + ret[k] = output{ + Value: ov, + Sensitive: v.Sensitive, + } + } + + return ret, nil +} + +func marshalRootModule(s *states.State, schemas *terraform.Schemas) (module, error) { + var ret module + var err error + + ret.Address = "" + ret.Resources, err = marshalResources(s.RootModule().Resources, schemas) + if err != nil { + return ret, err + } + + // build a map of module -> [child module addresses] + moduleMap := make(map[string][]addrs.ModuleInstance) + for _, mod := range s.Modules { + if mod.Addr.IsRoot() { + continue + } else { + parent := mod.Addr.Parent().String() + moduleMap[parent] = append(moduleMap[parent], mod.Addr) + } + } + + // use the state and module map to build up the module structure + ret.ChildModules, err = marshalModules(s, schemas, moduleMap[""], moduleMap) + return ret, err +} + +// marshalModules is an ungainly recursive function to build a module +// structure out of a teraform state. +func marshalModules( + s *states.State, + schemas *terraform.Schemas, + modules []addrs.ModuleInstance, + moduleMap map[string][]addrs.ModuleInstance, +) ([]module, error) { + var ret []module + for _, child := range modules { + stateMod := s.Module(child) + // cm for child module, naming things is hard. + cm := module{Address: stateMod.Addr.String()} + rs, err := marshalResources(stateMod.Resources, schemas) + if err != nil { + return nil, err + } + cm.Resources = rs + if moduleMap[child.String()] != nil { + moreChildModules, err := marshalModules(s, schemas, moduleMap[child.String()], moduleMap) + if err != nil { + return nil, err + } + cm.ChildModules = moreChildModules + } + + ret = append(ret, cm) + } + + return ret, nil +} + +func marshalResources(resources map[string]*states.Resource, schemas *terraform.Schemas) ([]resource, error) { + var ret []resource + + for _, r := range resources { + for k, ri := range r.Instances { + + resource := resource{ + Address: r.Addr.String(), + Type: r.Addr.Type, + Name: r.Addr.Name, + ProviderName: r.ProviderConfig.ProviderConfig.String(), + } + + switch r.Addr.Mode { + case addrs.ManagedResourceMode: + resource.Mode = "managed" + case addrs.DataResourceMode: + resource.Mode = "data" + default: + return ret, fmt.Errorf("resource %s has an unsupported mode %s", + r.Addr.String(), + r.Addr.Mode.String(), + ) + } + + if r.EachMode != states.NoEach { + resource.Index = k + } + + schema, _ := schemas.ResourceTypeConfig( + r.ProviderConfig.ProviderConfig.StringCompact(), + r.Addr.Mode, + r.Addr.Type, + ) + resource.SchemaVersion = ri.Current.SchemaVersion + + if schema == nil { + return nil, fmt.Errorf("no schema found for %s", r.Addr.String()) + } + riObj, err := ri.Current.Decode(schema.ImpliedType()) + if err != nil { + return nil, err + } + + resource.AttributeValues = marshalAttributeValues(riObj.Value, schema) + + ret = append(ret, resource) + } + + } + + return ret, nil +}