// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package tfjson import ( "bytes" "encoding/json" "errors" "fmt" "github.com/hashicorp/go-version" "github.com/zclconf/go-cty/cty" ) // StateFormatVersionConstraints defines the versions of the JSON state format // that are supported by this package. var StateFormatVersionConstraints = ">= 0.1, < 2.0" // State is the top-level representation of a Terraform state. type State struct { // useJSONNumber opts into the behavior of calling // json.Decoder.UseNumber prior to decoding the state, which turns // numbers into json.Numbers instead of float64s. Set it using // State.UseJSONNumber. useJSONNumber bool // The version of the state format. This should always match the // StateFormatVersion constant in this package, or else am // unmarshal will be unstable. FormatVersion string `json:"format_version,omitempty"` // The Terraform version used to make the state. TerraformVersion string `json:"terraform_version,omitempty"` // The values that make up the state. Values *StateValues `json:"values,omitempty"` // Checks contains the results of any conditional checks when Values was // last updated. Checks *CheckResultStatic `json:"checks,omitempty"` } // UseJSONNumber controls whether the State will be decoded using the // json.Number behavior or the float64 behavior. When b is true, the State will // represent numbers in StateOutputs as json.Numbers. When b is false, the // State will represent numbers in StateOutputs as float64s. func (s *State) UseJSONNumber(b bool) { s.useJSONNumber = b } // Validate checks to ensure that the state is present, and the // version matches the version supported by this library. func (s *State) Validate() error { if s == nil { return errors.New("state is nil") } if s.FormatVersion == "" { return errors.New("unexpected state input, format version is missing") } constraint, err := version.NewConstraint(StateFormatVersionConstraints) if err != nil { return fmt.Errorf("invalid version constraint: %w", err) } version, err := version.NewVersion(s.FormatVersion) if err != nil { return fmt.Errorf("invalid format version %q: %w", s.FormatVersion, err) } if !constraint.Check(version) { return fmt.Errorf("unsupported state format version: %q does not satisfy %q", version, constraint) } return nil } func (s *State) UnmarshalJSON(b []byte) error { type rawState State var state rawState dec := json.NewDecoder(bytes.NewReader(b)) if s.useJSONNumber { dec.UseNumber() } err := dec.Decode(&state) if err != nil { return err } *s = *(*State)(&state) return s.Validate() } // 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 { // The Outputs for this common state representation. Outputs map[string]*StateOutput `json:"outputs,omitempty"` // The root module in this state representation. RootModule *StateModule `json:"root_module,omitempty"` } // StateModule is the representation of a module in the common state // representation. This can be the root module or a child module. type StateModule struct { // All resources or data sources within this module. Resources []*StateResource `json:"resources,omitempty"` // The absolute module address, omitted for the root module. Address string `json:"address,omitempty"` // Any child modules within this module. ChildModules []*StateModule `json:"child_modules,omitempty"` } // StateResource is the representation of a resource in the common // state representation. type StateResource struct { // The absolute resource address. Address string `json:"address,omitempty"` // The resource mode. Mode ResourceMode `json:"mode,omitempty"` // The resource type, example: "aws_instance" for aws_instance.foo. Type string `json:"type,omitempty"` // The resource name, example: "foo" for aws_instance.foo. Name string `json:"name,omitempty"` // The instance key for any resources that have been created using // "count" or "for_each". If neither of these apply the key will be // empty. // // This value can be either an integer (int) or a string. Index interface{} `json:"index,omitempty"` // The name of the provider this resource belongs to. This allows // the provider to be interpreted unambiguously in the unusual // situation where a provider offers a resource type whose name // does not start with its own name, such as the "googlebeta" // provider offering "google_compute_instance". ProviderName string `json:"provider_name,omitempty"` // The version of the resource type schema the "values" property // conforms to. SchemaVersion uint64 `json:"schema_version,"` // 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. AttributeValues map[string]interface{} `json:"values,omitempty"` // The JSON representation of the sensitivity of the resource's // attribute values. Only attributes which are sensitive // are included in this structure. SensitiveValues json.RawMessage `json:"sensitive_values,omitempty"` // The addresses of the resources that this resource depends on. DependsOn []string `json:"depends_on,omitempty"` // If true, the resource has been marked as tainted and will be // re-created on the next update. Tainted bool `json:"tainted,omitempty"` // DeposedKey is set if the resource instance has been marked Deposed and // will be destroyed on the next apply. DeposedKey string `json:"deposed_key,omitempty"` } // StateOutput represents an output value in a common state // representation. type StateOutput struct { // Whether or not the output was marked as sensitive. Sensitive bool `json:"sensitive"` // The value of the output. Value interface{} `json:"value,omitempty"` // The type of the output. Type cty.Type `json:"type,omitempty"` } // jsonStateOutput describes an output value in a middle-step internal // representation before marshalled into a more useful StateOutput with cty.Type. // // This avoid panic on marshalling cty.NilType (from cty upstream) // which the default Go marshaller cannot ignore because it's a // not nil-able struct. type jsonStateOutput struct { Sensitive bool `json:"sensitive"` Value interface{} `json:"value,omitempty"` Type json.RawMessage `json:"type,omitempty"` } func (so *StateOutput) MarshalJSON() ([]byte, error) { jsonSa := &jsonStateOutput{ Sensitive: so.Sensitive, Value: so.Value, } if so.Type != cty.NilType { outputType, _ := so.Type.MarshalJSON() jsonSa.Type = outputType } return json.Marshal(jsonSa) }