github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/jsonstate/state.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package jsonstate
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"sort"
    10  
    11  	"github.com/zclconf/go-cty/cty"
    12  	ctyjson "github.com/zclconf/go-cty/cty/json"
    13  
    14  	"github.com/terramate-io/tf/addrs"
    15  	"github.com/terramate-io/tf/command/jsonchecks"
    16  	"github.com/terramate-io/tf/lang/marks"
    17  	"github.com/terramate-io/tf/states"
    18  	"github.com/terramate-io/tf/states/statefile"
    19  	"github.com/terramate-io/tf/terraform"
    20  )
    21  
    22  const (
    23  	// FormatVersion represents the version of the json format and will be
    24  	// incremented for any change to this format that requires changes to a
    25  	// consuming parser.
    26  	FormatVersion = "1.0"
    27  
    28  	ManagedResourceMode = "managed"
    29  	DataResourceMode    = "data"
    30  )
    31  
    32  // State is the top-level representation of the json format of a terraform
    33  // state.
    34  type State struct {
    35  	FormatVersion    string          `json:"format_version,omitempty"`
    36  	TerraformVersion string          `json:"terraform_version,omitempty"`
    37  	Values           *StateValues    `json:"values,omitempty"`
    38  	Checks           json.RawMessage `json:"checks,omitempty"`
    39  }
    40  
    41  // StateValues is the common representation of resolved values for both the prior
    42  // state (which is always complete) and the planned new state.
    43  type StateValues struct {
    44  	Outputs    map[string]Output `json:"outputs,omitempty"`
    45  	RootModule Module            `json:"root_module,omitempty"`
    46  }
    47  
    48  type Output struct {
    49  	Sensitive bool            `json:"sensitive"`
    50  	Value     json.RawMessage `json:"value,omitempty"`
    51  	Type      json.RawMessage `json:"type,omitempty"`
    52  }
    53  
    54  // Module is the representation of a module in state. This can be the root module
    55  // or a child module
    56  type Module struct {
    57  	// Resources are sorted in a user-friendly order that is undefined at this
    58  	// time, but consistent.
    59  	Resources []Resource `json:"resources,omitempty"`
    60  
    61  	// Address is the absolute module address, omitted for the root module
    62  	Address string `json:"address,omitempty"`
    63  
    64  	// Each module object can optionally have its own nested "child_modules",
    65  	// recursively describing the full module tree.
    66  	ChildModules []Module `json:"child_modules,omitempty"`
    67  }
    68  
    69  // Resource is the representation of a resource in the state.
    70  type Resource struct {
    71  	// Address is the absolute resource address
    72  	Address string `json:"address,omitempty"`
    73  
    74  	// Mode can be "managed" or "data"
    75  	Mode string `json:"mode,omitempty"`
    76  
    77  	Type string `json:"type,omitempty"`
    78  	Name string `json:"name,omitempty"`
    79  
    80  	// Index is omitted for a resource not using `count` or `for_each`.
    81  	Index json.RawMessage `json:"index,omitempty"`
    82  
    83  	// ProviderName allows the property "type" to be interpreted unambiguously
    84  	// in the unusual situation where a provider offers a resource type whose
    85  	// name does not start with its own name, such as the "googlebeta" provider
    86  	// offering "google_compute_instance".
    87  	ProviderName string `json:"provider_name"`
    88  
    89  	// SchemaVersion indicates which version of the resource type schema the
    90  	// "values" property conforms to.
    91  	SchemaVersion uint64 `json:"schema_version"`
    92  
    93  	// AttributeValues is the JSON representation of the attribute values of the
    94  	// resource, whose structure depends on the resource type schema. Any
    95  	// unknown values are omitted or set to null, making them indistinguishable
    96  	// from absent values.
    97  	AttributeValues AttributeValues `json:"values,omitempty"`
    98  
    99  	// SensitiveValues is similar to AttributeValues, but with all sensitive
   100  	// values replaced with true, and all non-sensitive leaf values omitted.
   101  	SensitiveValues json.RawMessage `json:"sensitive_values,omitempty"`
   102  
   103  	// DependsOn contains a list of the resource's dependencies. The entries are
   104  	// addresses relative to the containing module.
   105  	DependsOn []string `json:"depends_on,omitempty"`
   106  
   107  	// Tainted is true if the resource is tainted in terraform state.
   108  	Tainted bool `json:"tainted,omitempty"`
   109  
   110  	// Deposed is set if the resource is deposed in terraform state.
   111  	DeposedKey string `json:"deposed_key,omitempty"`
   112  }
   113  
   114  // AttributeValues is the JSON representation of the attribute values of the
   115  // resource, whose structure depends on the resource type schema.
   116  type AttributeValues map[string]json.RawMessage
   117  
   118  func marshalAttributeValues(value cty.Value) AttributeValues {
   119  	// unmark our value to show all values
   120  	value, _ = value.UnmarkDeep()
   121  
   122  	if value == cty.NilVal || value.IsNull() {
   123  		return nil
   124  	}
   125  
   126  	ret := make(AttributeValues)
   127  
   128  	it := value.ElementIterator()
   129  	for it.Next() {
   130  		k, v := it.Element()
   131  		vJSON, _ := ctyjson.Marshal(v, v.Type())
   132  		ret[k.AsString()] = json.RawMessage(vJSON)
   133  	}
   134  	return ret
   135  }
   136  
   137  // newState() returns a minimally-initialized state
   138  func newState() *State {
   139  	return &State{
   140  		FormatVersion: FormatVersion,
   141  	}
   142  }
   143  
   144  // MarshalForRenderer returns the pre-json encoding changes of the state, in a
   145  // format available to the structured renderer.
   146  func MarshalForRenderer(sf *statefile.File, schemas *terraform.Schemas) (Module, map[string]Output, error) {
   147  	if sf.State.Modules == nil {
   148  		// Empty state case.
   149  		return Module{}, nil, nil
   150  	}
   151  
   152  	outputs, err := MarshalOutputs(sf.State.RootModule().OutputValues)
   153  	if err != nil {
   154  		return Module{}, nil, err
   155  	}
   156  
   157  	root, err := marshalRootModule(sf.State, schemas)
   158  	if err != nil {
   159  		return Module{}, nil, err
   160  	}
   161  
   162  	return root, outputs, err
   163  }
   164  
   165  // MarshalForLog returns the origin JSON compatible state, read for a logging
   166  // package to marshal further.
   167  func MarshalForLog(sf *statefile.File, schemas *terraform.Schemas) (*State, error) {
   168  	output := newState()
   169  
   170  	if sf == nil || sf.State.Empty() {
   171  		return output, nil
   172  	}
   173  
   174  	if sf.TerraformVersion != nil {
   175  		output.TerraformVersion = sf.TerraformVersion.String()
   176  	}
   177  
   178  	// output.StateValues
   179  	err := output.marshalStateValues(sf.State, schemas)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	// output.Checks
   185  	if sf.State.CheckResults != nil && sf.State.CheckResults.ConfigResults.Len() > 0 {
   186  		output.Checks = jsonchecks.MarshalCheckStates(sf.State.CheckResults)
   187  	}
   188  
   189  	return output, nil
   190  }
   191  
   192  // Marshal returns the json encoding of a terraform state.
   193  func Marshal(sf *statefile.File, schemas *terraform.Schemas) ([]byte, error) {
   194  	output, err := MarshalForLog(sf, schemas)
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  
   199  	ret, err := json.Marshal(output)
   200  	return ret, err
   201  }
   202  
   203  func (jsonstate *State) marshalStateValues(s *states.State, schemas *terraform.Schemas) error {
   204  	var sv StateValues
   205  	var err error
   206  
   207  	// only marshal the root module outputs
   208  	sv.Outputs, err = MarshalOutputs(s.RootModule().OutputValues)
   209  	if err != nil {
   210  		return err
   211  	}
   212  
   213  	// use the state and module map to build up the module structure
   214  	sv.RootModule, err = marshalRootModule(s, schemas)
   215  	if err != nil {
   216  		return err
   217  	}
   218  
   219  	jsonstate.Values = &sv
   220  	return nil
   221  }
   222  
   223  // MarshalOutputs translates a map of states.OutputValue to a map of jsonstate.Output,
   224  // which are defined for json encoding.
   225  func MarshalOutputs(outputs map[string]*states.OutputValue) (map[string]Output, error) {
   226  	if outputs == nil {
   227  		return nil, nil
   228  	}
   229  
   230  	ret := make(map[string]Output)
   231  	for k, v := range outputs {
   232  		ty := v.Value.Type()
   233  		ov, err := ctyjson.Marshal(v.Value, ty)
   234  		if err != nil {
   235  			return ret, err
   236  		}
   237  		ot, err := ctyjson.MarshalType(ty)
   238  		if err != nil {
   239  			return ret, err
   240  		}
   241  		ret[k] = Output{
   242  			Value:     ov,
   243  			Type:      ot,
   244  			Sensitive: v.Sensitive,
   245  		}
   246  	}
   247  
   248  	return ret, nil
   249  }
   250  
   251  func marshalRootModule(s *states.State, schemas *terraform.Schemas) (Module, error) {
   252  	var ret Module
   253  	var err error
   254  
   255  	ret.Address = ""
   256  	rs, err := marshalResources(s.RootModule().Resources, addrs.RootModuleInstance, schemas)
   257  	if err != nil {
   258  		return ret, err
   259  	}
   260  	ret.Resources = rs
   261  
   262  	// build a map of module -> set[child module addresses]
   263  	moduleChildSet := make(map[string]map[string]struct{})
   264  	for _, mod := range s.Modules {
   265  		if mod.Addr.IsRoot() {
   266  			continue
   267  		} else {
   268  			for childAddr := mod.Addr; !childAddr.IsRoot(); childAddr = childAddr.Parent() {
   269  				if _, ok := moduleChildSet[childAddr.Parent().String()]; !ok {
   270  					moduleChildSet[childAddr.Parent().String()] = map[string]struct{}{}
   271  				}
   272  				moduleChildSet[childAddr.Parent().String()][childAddr.String()] = struct{}{}
   273  			}
   274  		}
   275  	}
   276  
   277  	// transform the previous map into map of module -> [child module addresses]
   278  	moduleMap := make(map[string][]addrs.ModuleInstance)
   279  	for parent, children := range moduleChildSet {
   280  		for child := range children {
   281  			childModuleInstance, diags := addrs.ParseModuleInstanceStr(child)
   282  			if diags.HasErrors() {
   283  				return ret, diags.Err()
   284  			}
   285  			moduleMap[parent] = append(moduleMap[parent], childModuleInstance)
   286  		}
   287  	}
   288  
   289  	// use the state and module map to build up the module structure
   290  	ret.ChildModules, err = marshalModules(s, schemas, moduleMap[""], moduleMap)
   291  	return ret, err
   292  }
   293  
   294  // marshalModules is an ungainly recursive function to build a module structure
   295  // out of terraform state.
   296  func marshalModules(
   297  	s *states.State,
   298  	schemas *terraform.Schemas,
   299  	modules []addrs.ModuleInstance,
   300  	moduleMap map[string][]addrs.ModuleInstance,
   301  ) ([]Module, error) {
   302  	var ret []Module
   303  	for _, child := range modules {
   304  		// cm for child module, naming things is hard.
   305  		cm := Module{Address: child.String()}
   306  
   307  		// the module may be resourceless and contain only submodules, it will then be nil here
   308  		stateMod := s.Module(child)
   309  		if stateMod != nil {
   310  			rs, err := marshalResources(stateMod.Resources, stateMod.Addr, schemas)
   311  			if err != nil {
   312  				return nil, err
   313  			}
   314  			cm.Resources = rs
   315  		}
   316  
   317  		if moduleMap[child.String()] != nil {
   318  			moreChildModules, err := marshalModules(s, schemas, moduleMap[child.String()], moduleMap)
   319  			if err != nil {
   320  				return nil, err
   321  			}
   322  			cm.ChildModules = moreChildModules
   323  		}
   324  
   325  		ret = append(ret, cm)
   326  	}
   327  
   328  	// sort the child modules by address for consistency.
   329  	sort.Slice(ret, func(i, j int) bool {
   330  		return ret[i].Address < ret[j].Address
   331  	})
   332  
   333  	return ret, nil
   334  }
   335  
   336  func marshalResources(resources map[string]*states.Resource, module addrs.ModuleInstance, schemas *terraform.Schemas) ([]Resource, error) {
   337  	var ret []Resource
   338  
   339  	var sortedResources []*states.Resource
   340  	for _, r := range resources {
   341  		sortedResources = append(sortedResources, r)
   342  	}
   343  	sort.Slice(sortedResources, func(i, j int) bool {
   344  		return sortedResources[i].Addr.Less(sortedResources[j].Addr)
   345  	})
   346  
   347  	for _, r := range sortedResources {
   348  
   349  		var sortedKeys []addrs.InstanceKey
   350  		for k := range r.Instances {
   351  			sortedKeys = append(sortedKeys, k)
   352  		}
   353  		sort.Slice(sortedKeys, func(i, j int) bool {
   354  			return addrs.InstanceKeyLess(sortedKeys[i], sortedKeys[j])
   355  		})
   356  
   357  		for _, k := range sortedKeys {
   358  			ri := r.Instances[k]
   359  
   360  			var err error
   361  
   362  			resAddr := r.Addr.Resource
   363  
   364  			current := Resource{
   365  				Address:      r.Addr.Instance(k).String(),
   366  				Type:         resAddr.Type,
   367  				Name:         resAddr.Name,
   368  				ProviderName: r.ProviderConfig.Provider.String(),
   369  			}
   370  
   371  			if k != nil {
   372  				index := k.Value()
   373  				if current.Index, err = ctyjson.Marshal(index, index.Type()); err != nil {
   374  					return nil, err
   375  				}
   376  			}
   377  
   378  			switch resAddr.Mode {
   379  			case addrs.ManagedResourceMode:
   380  				current.Mode = ManagedResourceMode
   381  			case addrs.DataResourceMode:
   382  				current.Mode = DataResourceMode
   383  			default:
   384  				return ret, fmt.Errorf("resource %s has an unsupported mode %s",
   385  					resAddr.String(),
   386  					resAddr.Mode.String(),
   387  				)
   388  			}
   389  
   390  			schema, version := schemas.ResourceTypeConfig(
   391  				r.ProviderConfig.Provider,
   392  				resAddr.Mode,
   393  				resAddr.Type,
   394  			)
   395  
   396  			// It is possible that the only instance is deposed
   397  			if ri.Current != nil {
   398  				if version != ri.Current.SchemaVersion {
   399  					return nil, fmt.Errorf("schema version %d for %s in state does not match version %d from the provider", ri.Current.SchemaVersion, resAddr, version)
   400  				}
   401  
   402  				current.SchemaVersion = ri.Current.SchemaVersion
   403  
   404  				if schema == nil {
   405  					return nil, fmt.Errorf("no schema found for %s (in provider %s)", resAddr.String(), r.ProviderConfig.Provider)
   406  				}
   407  				riObj, err := ri.Current.Decode(schema.ImpliedType())
   408  				if err != nil {
   409  					return nil, err
   410  				}
   411  
   412  				current.AttributeValues = marshalAttributeValues(riObj.Value)
   413  
   414  				value, marks := riObj.Value.UnmarkDeepWithPaths()
   415  				if schema.ContainsSensitive() {
   416  					marks = append(marks, schema.ValueMarks(value, nil)...)
   417  				}
   418  				s := SensitiveAsBool(value.MarkWithPaths(marks))
   419  				v, err := ctyjson.Marshal(s, s.Type())
   420  				if err != nil {
   421  					return nil, err
   422  				}
   423  				current.SensitiveValues = v
   424  
   425  				if len(riObj.Dependencies) > 0 {
   426  					dependencies := make([]string, len(riObj.Dependencies))
   427  					for i, v := range riObj.Dependencies {
   428  						dependencies[i] = v.String()
   429  					}
   430  					current.DependsOn = dependencies
   431  				}
   432  
   433  				if riObj.Status == states.ObjectTainted {
   434  					current.Tainted = true
   435  				}
   436  				ret = append(ret, current)
   437  			}
   438  
   439  			var sortedDeposedKeys []string
   440  			for k := range ri.Deposed {
   441  				sortedDeposedKeys = append(sortedDeposedKeys, string(k))
   442  			}
   443  			sort.Strings(sortedDeposedKeys)
   444  
   445  			for _, deposedKey := range sortedDeposedKeys {
   446  				rios := ri.Deposed[states.DeposedKey(deposedKey)]
   447  
   448  				// copy the base fields from the current instance
   449  				deposed := Resource{
   450  					Address:      current.Address,
   451  					Type:         current.Type,
   452  					Name:         current.Name,
   453  					ProviderName: current.ProviderName,
   454  					Mode:         current.Mode,
   455  					Index:        current.Index,
   456  				}
   457  
   458  				riObj, err := rios.Decode(schema.ImpliedType())
   459  				if err != nil {
   460  					return nil, err
   461  				}
   462  
   463  				deposed.AttributeValues = marshalAttributeValues(riObj.Value)
   464  
   465  				value, marks := riObj.Value.UnmarkDeepWithPaths()
   466  				if schema.ContainsSensitive() {
   467  					marks = append(marks, schema.ValueMarks(value, nil)...)
   468  				}
   469  				s := SensitiveAsBool(value.MarkWithPaths(marks))
   470  				v, err := ctyjson.Marshal(s, s.Type())
   471  				if err != nil {
   472  					return nil, err
   473  				}
   474  				deposed.SensitiveValues = v
   475  
   476  				if len(riObj.Dependencies) > 0 {
   477  					dependencies := make([]string, len(riObj.Dependencies))
   478  					for i, v := range riObj.Dependencies {
   479  						dependencies[i] = v.String()
   480  					}
   481  					deposed.DependsOn = dependencies
   482  				}
   483  
   484  				if riObj.Status == states.ObjectTainted {
   485  					deposed.Tainted = true
   486  				}
   487  				deposed.DeposedKey = deposedKey
   488  				ret = append(ret, deposed)
   489  			}
   490  		}
   491  	}
   492  
   493  	return ret, nil
   494  }
   495  
   496  func SensitiveAsBool(val cty.Value) cty.Value {
   497  	if val.HasMark(marks.Sensitive) {
   498  		return cty.True
   499  	}
   500  
   501  	ty := val.Type()
   502  	switch {
   503  	case val.IsNull(), ty.IsPrimitiveType(), ty.Equals(cty.DynamicPseudoType):
   504  		return cty.False
   505  	case ty.IsListType() || ty.IsTupleType() || ty.IsSetType():
   506  		if !val.IsKnown() {
   507  			// If the collection is unknown we can't say anything about the
   508  			// sensitivity of its contents
   509  			return cty.EmptyTupleVal
   510  		}
   511  		length := val.LengthInt()
   512  		if length == 0 {
   513  			// If there are no elements then we can't have sensitive values
   514  			return cty.EmptyTupleVal
   515  		}
   516  		vals := make([]cty.Value, 0, length)
   517  		it := val.ElementIterator()
   518  		for it.Next() {
   519  			_, v := it.Element()
   520  			vals = append(vals, SensitiveAsBool(v))
   521  		}
   522  		// The above transform may have changed the types of some of the
   523  		// elements, so we'll always use a tuple here in case we've now made
   524  		// different elements have different types. Our ultimate goal is to
   525  		// marshal to JSON anyway, and all of these sequence types are
   526  		// indistinguishable in JSON.
   527  		return cty.TupleVal(vals)
   528  	case ty.IsMapType() || ty.IsObjectType():
   529  		if !val.IsKnown() {
   530  			// If the map/object is unknown we can't say anything about the
   531  			// sensitivity of its attributes
   532  			return cty.EmptyObjectVal
   533  		}
   534  		var length int
   535  		switch {
   536  		case ty.IsMapType():
   537  			length = val.LengthInt()
   538  		default:
   539  			length = len(val.Type().AttributeTypes())
   540  		}
   541  		if length == 0 {
   542  			// If there are no elements then we can't have sensitive values
   543  			return cty.EmptyObjectVal
   544  		}
   545  		vals := make(map[string]cty.Value)
   546  		it := val.ElementIterator()
   547  		for it.Next() {
   548  			k, v := it.Element()
   549  			s := SensitiveAsBool(v)
   550  			// Omit all of the "false"s for non-sensitive values for more
   551  			// compact serialization
   552  			if !s.RawEquals(cty.False) {
   553  				vals[k.AsString()] = s
   554  			}
   555  		}
   556  		// The above transform may have changed the types of some of the
   557  		// elements, so we'll always use an object here in case we've now made
   558  		// different elements have different types. Our ultimate goal is to
   559  		// marshal to JSON anyway, and all of these mapping types are
   560  		// indistinguishable in JSON.
   561  		return cty.ObjectVal(vals)
   562  	default:
   563  		// Should never happen, since the above should cover all types
   564  		panic(fmt.Sprintf("sensitiveAsBool cannot handle %#v", val))
   565  	}
   566  }