github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/command/jsonstate/state.go (about)

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