remove legacy state types

This commit is contained in:
James Bardin 2020-11-13 13:59:11 -05:00
parent 943ef51d67
commit 14dedf295b
21 changed files with 135 additions and 7373 deletions

View File

@ -1,7 +1,6 @@
package terraform
import (
"bytes"
"context"
"fmt"
"log"
@ -17,7 +16,6 @@ import (
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/provisioners"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/states/statefile"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
@ -897,37 +895,3 @@ func (c *Context) watchStop(walker *ContextGraphWalker) (chan struct{}, <-chan s
return stop, wait
}
// ShimLegacyState is a helper that takes the legacy state type and
// converts it to the new state type.
//
// This is implemented as a state file upgrade, so it will not preserve
// parts of the state structure that are not included in a serialized state,
// such as the resolved results of any local values, outputs in non-root
// modules, etc.
func ShimLegacyState(legacy *State) (*states.State, error) {
if legacy == nil {
return nil, nil
}
var buf bytes.Buffer
err := WriteState(legacy, &buf)
if err != nil {
return nil, err
}
f, err := statefile.Read(&buf)
if err != nil {
return nil, err
}
return f.State, err
}
// MustShimLegacyState is a wrapper around ShimLegacyState that panics if
// the conversion does not succeed. This is primarily intended for tests where
// the given legacy state is an object constructed within the test.
func MustShimLegacyState(legacy *State) *states.State {
ret, err := ShimLegacyState(legacy)
if err != nil {
panic(err)
}
return ret
}

View File

@ -3484,7 +3484,7 @@ func TestContext2Apply_multiVarComprehensive(t *testing.T) {
m := testModule(t, "apply-multi-var-comprehensive")
p := testProvider("test")
configs := map[string]*ResourceConfig{}
configs := map[string]cty.Value{}
var configsLock sync.Mutex
p.ApplyResourceChangeFn = testApplyFn
@ -3498,7 +3498,7 @@ func TestContext2Apply_multiVarComprehensive(t *testing.T) {
// and so the assertions below expect an old-style ResourceConfig, which
// we'll construct via our shim for now to avoid rewriting all of the
// assertions.
configs[key] = NewResourceConfigShimmed(req.Config, p.GetSchemaReturn.ResourceTypes["test_thing"])
configs[key] = req.ProposedNewState
retVals := make(map[string]cty.Value)
for it := proposed.ElementIterator(); it.Next(); {
@ -3563,102 +3563,99 @@ func TestContext2Apply_multiVarComprehensive(t *testing.T) {
t.Fatalf("errors during plan")
}
checkConfig := func(key string, want map[string]interface{}) {
checkConfig := func(key string, want cty.Value) {
configsLock.Lock()
defer configsLock.Unlock()
if _, ok := configs[key]; !ok {
got, ok := configs[key]
if !ok {
t.Errorf("no config recorded for %s; expected a configuration", key)
return
}
got := configs[key].Config
t.Run("config for "+key, func(t *testing.T) {
want["key"] = key // to avoid doing this for every example
for _, problem := range deep.Equal(got, want) {
t.Errorf(problem)
}
})
}
checkConfig("multi_count_var.0", map[string]interface{}{
"source_id": hcl2shim.UnknownVariableValue,
"source_name": "source.0",
})
checkConfig("multi_count_var.2", map[string]interface{}{
"source_id": hcl2shim.UnknownVariableValue,
"source_name": "source.2",
})
checkConfig("multi_count_derived.0", map[string]interface{}{
"source_id": hcl2shim.UnknownVariableValue,
"source_name": "source.0",
})
checkConfig("multi_count_derived.2", map[string]interface{}{
"source_id": hcl2shim.UnknownVariableValue,
"source_name": "source.2",
})
checkConfig("whole_splat", map[string]interface{}{
"source_ids": []interface{}{
hcl2shim.UnknownVariableValue,
hcl2shim.UnknownVariableValue,
hcl2shim.UnknownVariableValue,
},
"source_names": []interface{}{
"source.0",
"source.1",
"source.2",
},
"source_ids_from_func": hcl2shim.UnknownVariableValue,
"source_names_from_func": []interface{}{
"source.0",
"source.1",
"source.2",
},
"source_ids_wrapped": []interface{}{
[]interface{}{
hcl2shim.UnknownVariableValue,
hcl2shim.UnknownVariableValue,
hcl2shim.UnknownVariableValue,
},
},
"source_names_wrapped": []interface{}{
[]interface{}{
"source.0",
"source.1",
"source.2",
},
},
"first_source_id": hcl2shim.UnknownVariableValue,
"first_source_name": "source.0",
})
checkConfig("child.whole_splat", map[string]interface{}{
"source_ids": []interface{}{
hcl2shim.UnknownVariableValue,
hcl2shim.UnknownVariableValue,
hcl2shim.UnknownVariableValue,
},
"source_names": []interface{}{
"source.0",
"source.1",
"source.2",
},
"source_ids_wrapped": []interface{}{
[]interface{}{
hcl2shim.UnknownVariableValue,
hcl2shim.UnknownVariableValue,
hcl2shim.UnknownVariableValue,
},
},
"source_names_wrapped": []interface{}{
[]interface{}{
"source.0",
"source.1",
"source.2",
},
},
})
checkConfig("multi_count_var.0", cty.ObjectVal(map[string]cty.Value{
"source_id": cty.UnknownVal(cty.String),
"source_name": cty.StringVal("source.0"),
}))
checkConfig("multi_count_var.2", cty.ObjectVal(map[string]cty.Value{
"source_id": cty.UnknownVal(cty.String),
"source_name": cty.StringVal("source.2"),
}))
checkConfig("multi_count_derived.0", cty.ObjectVal(map[string]cty.Value{
"source_id": cty.UnknownVal(cty.String),
"source_name": cty.StringVal("source.0"),
}))
checkConfig("multi_count_derived.2", cty.ObjectVal(map[string]cty.Value{
"source_id": cty.UnknownVal(cty.String),
"source_name": cty.StringVal("source.2"),
}))
checkConfig("whole_splat", cty.ObjectVal(map[string]cty.Value{
"source_ids": cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
}),
"source_names": cty.ListVal([]cty.Value{
cty.StringVal("source.0"),
cty.StringVal("source.1"),
cty.StringVal("source.2"),
}),
"source_ids_from_func": cty.UnknownVal(cty.String),
"source_names_from_func": cty.ListVal([]cty.Value{
cty.StringVal("source.0"),
cty.StringVal("source.1"),
cty.StringVal("source.2"),
}),
"source_ids_wrapped": cty.ListVal([]cty.Value{
cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
}),
}),
"source_names_wrapped": cty.ListVal([]cty.Value{
cty.ListVal([]cty.Value{
cty.StringVal("source.0"),
cty.StringVal("source.1"),
cty.StringVal("source.2"),
}),
}),
"first_source_id": cty.UnknownVal(cty.String),
"first_source_name": cty.StringVal("source.0"),
}))
checkConfig("child.whole_splat", cty.ObjectVal(map[string]cty.Value{
"source_ids": cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
}),
"source_names": cty.ListVal([]cty.Value{
cty.StringVal("source.0"),
cty.StringVal("source.1"),
cty.StringVal("source.2"),
}),
"source_ids_wrapped": cty.ListVal([]cty.Value{
cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
}),
}),
"source_names_wrapped": cty.ListVal([]cty.Value{
cty.ListVal([]cty.Value{
cty.StringVal("source.0"),
cty.StringVal("source.1"),
cty.StringVal("source.2"),
}),
}),
}))
t.Run("apply", func(t *testing.T) {
state, diags := ctx.Apply()

View File

@ -443,20 +443,6 @@ func checkStateString(t *testing.T, state *states.State, expected string) {
}
}
func resourceState(resourceType, resourceID string) *ResourceState {
providerResource := strings.Split(resourceType, "_")
return &ResourceState{
Type: resourceType,
Primary: &InstanceState{
ID: resourceID,
Attributes: map[string]string{
"id": resourceID,
},
},
Provider: "provider." + providerResource[0],
}
}
// Test helper that gives a function 3 seconds to finish, assumes deadlock and
// fails test if it does not.
func testCheckDeadlock(t *testing.T, f func()) {

View File

@ -103,18 +103,6 @@ func (d *Diff) AddModule(path addrs.ModuleInstance) *ModuleDiff {
// This should be the preferred lookup mechanism as it allows for future
// lookup optimizations.
func (d *Diff) ModuleByPath(path addrs.ModuleInstance) *ModuleDiff {
if d == nil {
return nil
}
for _, mod := range d.Modules {
if mod.Path == nil {
panic("missing module path")
}
modPath := normalizeModulePath(mod.Path)
if modPath.String() == path.String() {
return mod
}
}
return nil
}
@ -190,7 +178,7 @@ func (d *Diff) String() string {
keys := make([]string, 0, len(d.Modules))
lookup := make(map[string]*ModuleDiff)
for _, m := range d.Modules {
addr := normalizeModulePath(m.Path)
addr := addrs.ModuleInstance{} //normalizeModulePath(m.Path)
key := addr.String()
keys = append(keys, key)
lookup[key] = m
@ -201,11 +189,11 @@ func (d *Diff) String() string {
m := lookup[key]
mStr := m.String()
// If we're the root module, we just write the output directly.
if reflect.DeepEqual(m.Path, rootModulePath) {
buf.WriteString(mStr + "\n")
continue
}
//// If we're the root module, we just write the output directly.
//if reflect.DeepEqual(m.Path, rootModulePath) {
// buf.WriteString(mStr + "\n")
// continue
//}
buf.WriteString(fmt.Sprintf("%s:\n", key))
@ -219,13 +207,6 @@ func (d *Diff) String() string {
}
func (d *Diff) init() {
if d.Modules == nil {
rootDiff := &ModuleDiff{Path: rootModulePath}
d.Modules = []*ModuleDiff{rootDiff}
}
for _, m := range d.Modules {
m.init()
}
}
// ModuleDiff tracks the differences between resources to apply within
@ -304,7 +285,7 @@ func (d *ModuleDiff) Instances(id string) []*InstanceDiff {
// IsRoot says whether or not this module diff is for the root module.
func (d *ModuleDiff) IsRoot() bool {
return reflect.DeepEqual(d.Path, rootModulePath)
panic("not implemented")
}
// String outputs the diff in a long but command-line friendly output

View File

@ -26,18 +26,28 @@ func TestEvalReadState(t *testing.T) {
provider := providers.Interface(mockProvider)
cases := map[string]struct {
Resources map[string]*ResourceState
State *states.State
Node *EvalReadState
ExpectedInstanceId string
}{
"ReadState gets primary instance state": {
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Primary: &InstanceState{
ID: "i-abc123",
},
},
},
State: states.BuildState(func(s *states.SyncState) {
providerAddr := addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
}
oneAddr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "bar",
}.Absolute(addrs.RootModuleInstance)
s.SetResourceProvider(oneAddr, providerAddr)
s.SetResourceInstanceCurrent(oneAddr.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-abc123"}`),
}, providerAddr)
}),
Node: &EvalReadState{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
@ -56,15 +66,7 @@ func TestEvalReadState(t *testing.T) {
for k, c := range cases {
t.Run(k, func(t *testing.T) {
ctx := new(MockEvalContext)
state := MustShimLegacyState(&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: c.Resources,
},
},
})
ctx.StateState = state.SyncWrapper()
ctx.StateState = c.State.SyncWrapper()
ctx.PathPath = addrs.RootModuleInstance
diags := c.Node.Eval(ctx)
@ -97,18 +99,28 @@ func TestEvalReadStateDeposed(t *testing.T) {
provider := providers.Interface(mockProvider)
cases := map[string]struct {
Resources map[string]*ResourceState
State *states.State
Node *EvalReadStateDeposed
ExpectedInstanceId string
}{
"ReadStateDeposed gets deposed instance": {
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Deposed: []*InstanceState{
&InstanceState{ID: "i-abc123"},
},
},
},
State: states.BuildState(func(s *states.SyncState) {
providerAddr := addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
}
oneAddr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "bar",
}.Absolute(addrs.RootModuleInstance)
s.SetResourceProvider(oneAddr, providerAddr)
s.SetResourceInstanceDeposed(oneAddr.Instance(addrs.NoKey), states.DeposedKey("00000001"), &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-abc123"}`),
}, providerAddr)
}),
Node: &EvalReadStateDeposed{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
@ -127,15 +139,7 @@ func TestEvalReadStateDeposed(t *testing.T) {
for k, c := range cases {
t.Run(k, func(t *testing.T) {
ctx := new(MockEvalContext)
state := MustShimLegacyState(&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: c.Resources,
},
},
})
ctx.StateState = state.SyncWrapper()
ctx.StateState = c.State.SyncWrapper()
ctx.PathPath = addrs.RootModuleInstance
diags := c.Node.Eval(ctx)

View File

@ -1,122 +0,0 @@
package terraform
import (
"bytes"
"encoding/gob"
"fmt"
"io"
"sync"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/configs"
)
func init() {
gob.Register(make([]interface{}, 0))
gob.Register(make([]map[string]interface{}, 0))
gob.Register(make(map[string]interface{}))
gob.Register(make(map[string]string))
}
// Plan represents a single Terraform execution plan, which contains
// all the information necessary to make an infrastructure change.
//
// A plan has to contain basically the entire state of the world
// necessary to make a change: the state, diff, config, backend config, etc.
// This is so that it can run alone without any other data.
type Plan struct {
// Diff describes the resource actions that must be taken when this
// plan is applied.
Diff *Diff
// Config represents the entire configuration that was present when this
// plan was created.
Config *configs.Config
// State is the Terraform state that was current when this plan was
// created.
//
// It is not allowed to apply a plan that has a stale state, since its
// diff could be outdated.
State *State
// Vars retains the variables that were set when creating the plan, so
// that the same variables can be applied during apply.
Vars map[string]cty.Value
// Targets, if non-empty, contains a set of resource address strings that
// identify graph nodes that were selected as targets for plan.
//
// When targets are set, any graph node that is not directly targeted or
// indirectly targeted via dependencies is excluded from the graph.
Targets []string
// TerraformVersion is the version of Terraform that was used to create
// this plan.
//
// It is not allowed to apply a plan created with a different version of
// Terraform, since the other fields of this structure may be interpreted
// in different ways between versions.
TerraformVersion string
// ProviderSHA256s is a map giving the SHA256 hashes of the exact binaries
// used as plugins for each provider during plan.
//
// These must match between plan and apply to ensure that the diff is
// correctly interpreted, since different provider versions may have
// different attributes or attribute value constraints.
ProviderSHA256s map[string][]byte
// Backend is the backend that this plan should use and store data with.
Backend *BackendState
// Destroy indicates that this plan was created for a full destroy operation
Destroy bool
once sync.Once
}
func (p *Plan) String() string {
buf := new(bytes.Buffer)
buf.WriteString("DIFF:\n\n")
buf.WriteString(p.Diff.String())
buf.WriteString("\n\nSTATE:\n\n")
buf.WriteString(p.State.String())
return buf.String()
}
func (p *Plan) init() {
p.once.Do(func() {
if p.Diff == nil {
p.Diff = new(Diff)
p.Diff.init()
}
if p.State == nil {
p.State = new(State)
p.State.init()
}
if p.Vars == nil {
p.Vars = make(map[string]cty.Value)
}
})
}
// The format byte is prefixed into the plan file format so that we have
// the ability in the future to change the file format if we want for any
// reason.
const planFormatMagic = "tfplan"
const planFormatVersion byte = 2
// ReadPlan reads a plan structure out of a reader in the format that
// was written by WritePlan.
func ReadPlan(src io.Reader) (*Plan, error) {
return nil, fmt.Errorf("terraform.ReadPlan is no longer in use; use planfile.Open instead")
}
// WritePlan writes a plan somewhere in a binary format.
func WritePlan(d *Plan, dst io.Writer) error {
return fmt.Errorf("terraform.WritePlan is no longer in use; use planfile.Create instead")
}

View File

@ -1,516 +0,0 @@
package terraform
import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"github.com/mitchellh/copystructure"
"github.com/mitchellh/reflectwalk"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/configs/hcl2shim"
)
// Resource is a legacy way to identify a particular resource instance.
//
// New code should use addrs.ResourceInstance instead. This is still here
// only for codepaths that haven't been updated yet.
type Resource struct {
// These are all used by the new EvalNode stuff.
Name string
Type string
CountIndex int
// These aren't really used anymore anywhere, but we keep them around
// since we haven't done a proper cleanup yet.
Id string
Info *InstanceInfo
Config *ResourceConfig
Dependencies []string
Diff *InstanceDiff
Provider ResourceProvider
State *InstanceState
Flags ResourceFlag
}
// NewResource constructs a legacy Resource object from an
// addrs.ResourceInstance value.
//
// This is provided to shim to old codepaths that haven't been updated away
// from this type yet. Since this old type is not able to represent instances
// that have string keys, this function will panic if given a resource address
// that has a string key.
func NewResource(addr addrs.ResourceInstance) *Resource {
ret := &Resource{
Name: addr.Resource.Name,
Type: addr.Resource.Type,
}
if addr.Key != addrs.NoKey {
switch tk := addr.Key.(type) {
case addrs.IntKey:
ret.CountIndex = int(tk)
default:
panic(fmt.Errorf("resource instance with key %#v is not supported", addr.Key))
}
}
return ret
}
// ResourceKind specifies what kind of instance we're working with, whether
// its a primary instance, a tainted instance, or an orphan.
type ResourceFlag byte
// InstanceInfo is used to hold information about the instance and/or
// resource being modified.
type InstanceInfo struct {
// Id is a unique name to represent this instance. This is not related
// to InstanceState.ID in any way.
Id string
// ModulePath is the complete path of the module containing this
// instance.
ModulePath []string
// Type is the resource type of this instance
Type string
// uniqueExtra is an internal field that can be populated to supply
// extra metadata that is used to identify a unique instance in
// the graph walk. This will be appended to HumanID when uniqueId
// is called.
uniqueExtra string
}
// NewInstanceInfo constructs an InstanceInfo from an addrs.AbsResourceInstance.
//
// InstanceInfo is a legacy type, and uses of it should be gradually replaced
// by direct use of addrs.AbsResource or addrs.AbsResourceInstance as
// appropriate.
//
// The legacy InstanceInfo type cannot represent module instances with instance
// keys, so this function will panic if given such a path. Uses of this type
// should all be removed or replaced before implementing "count" and "for_each"
// arguments on modules in order to avoid such panics.
//
// This legacy type also cannot represent resource instances with string
// instance keys. It will panic if the given key is not either NoKey or an
// IntKey.
func NewInstanceInfo(addr addrs.AbsResourceInstance) *InstanceInfo {
// We need an old-style []string module path for InstanceInfo.
path := make([]string, len(addr.Module))
for i, step := range addr.Module {
if step.InstanceKey != addrs.NoKey {
panic("NewInstanceInfo cannot convert module instance with key")
}
path[i] = step.Name
}
// This is a funny old meaning of "id" that is no longer current. It should
// not be used for anything users might see. Note that it does not include
// a representation of the resource mode, and so it's impossible to
// determine from an InstanceInfo alone whether it is a managed or data
// resource that is being referred to.
id := fmt.Sprintf("%s.%s", addr.Resource.Resource.Type, addr.Resource.Resource.Name)
if addr.Resource.Resource.Mode == addrs.DataResourceMode {
id = "data." + id
}
if addr.Resource.Key != addrs.NoKey {
switch k := addr.Resource.Key.(type) {
case addrs.IntKey:
id = id + fmt.Sprintf(".%d", int(k))
default:
panic(fmt.Sprintf("NewInstanceInfo cannot convert resource instance with %T instance key", addr.Resource.Key))
}
}
return &InstanceInfo{
Id: id,
ModulePath: path,
Type: addr.Resource.Resource.Type,
}
}
// ResourceAddress returns the address of the resource that the receiver is describing.
func (i *InstanceInfo) ResourceAddress() *ResourceAddress {
// GROSS: for tainted and deposed instances, their status gets appended
// to i.Id to create a unique id for the graph node. Historically these
// ids were displayed to the user, so it's designed to be human-readable:
// "aws_instance.bar.0 (deposed #0)"
//
// So here we detect such suffixes and try to interpret them back to
// their original meaning so we can then produce a ResourceAddress
// with a suitable InstanceType.
id := i.Id
instanceType := TypeInvalid
if idx := strings.Index(id, " ("); idx != -1 {
remain := id[idx:]
id = id[:idx]
switch {
case strings.Contains(remain, "tainted"):
instanceType = TypeTainted
case strings.Contains(remain, "deposed"):
instanceType = TypeDeposed
}
}
addr, err := parseResourceAddressInternal(id)
if err != nil {
// should never happen, since that would indicate a bug in the
// code that constructed this InstanceInfo.
panic(fmt.Errorf("InstanceInfo has invalid Id %s", id))
}
if len(i.ModulePath) > 1 {
addr.Path = i.ModulePath[1:] // trim off "root" prefix, which is implied
}
if instanceType != TypeInvalid {
addr.InstanceTypeSet = true
addr.InstanceType = instanceType
}
return addr
}
// ResourceConfig is a legacy type that was formerly used to represent
// interpolatable configuration blocks. It is now only used to shim to old
// APIs that still use this type, via NewResourceConfigShimmed.
type ResourceConfig struct {
ComputedKeys []string
Raw map[string]interface{}
Config map[string]interface{}
}
// NewResourceConfigRaw constructs a ResourceConfig whose content is exactly
// the given value.
//
// The given value may contain hcl2shim.UnknownVariableValue to signal that
// something is computed, but it must not contain unprocessed interpolation
// sequences as we might've seen in Terraform v0.11 and prior.
func NewResourceConfigRaw(raw map[string]interface{}) *ResourceConfig {
v := hcl2shim.HCL2ValueFromConfigValue(raw)
// This is a little weird but we round-trip the value through the hcl2shim
// package here for two reasons: firstly, because that reduces the risk
// of it including something unlike what NewResourceConfigShimmed would
// produce, and secondly because it creates a copy of "raw" just in case
// something is relying on the fact that in the old world the raw and
// config maps were always distinct, and thus you could in principle mutate
// one without affecting the other. (I sure hope nobody was doing that, though!)
cfg := hcl2shim.ConfigValueFromHCL2(v).(map[string]interface{})
return &ResourceConfig{
Raw: raw,
Config: cfg,
ComputedKeys: newResourceConfigShimmedComputedKeys(v, ""),
}
}
// NewResourceConfigShimmed wraps a cty.Value of object type in a legacy
// ResourceConfig object, so that it can be passed to older APIs that expect
// this wrapping.
//
// The returned ResourceConfig is already interpolated and cannot be
// re-interpolated. It is, therefore, useful only to functions that expect
// an already-populated ResourceConfig which they then treat as read-only.
//
// If the given value is not of an object type that conforms to the given
// schema then this function will panic.
func NewResourceConfigShimmed(val cty.Value, schema *configschema.Block) *ResourceConfig {
if !val.Type().IsObjectType() {
panic(fmt.Errorf("NewResourceConfigShimmed given %#v; an object type is required", val.Type()))
}
ret := &ResourceConfig{}
legacyVal := hcl2shim.ConfigValueFromHCL2Block(val, schema)
if legacyVal != nil {
ret.Config = legacyVal
// Now we need to walk through our structure and find any unknown values,
// producing the separate list ComputedKeys to represent these. We use the
// schema here so that we can preserve the expected invariant
// that an attribute is always either wholly known or wholly unknown, while
// a child block can be partially unknown.
ret.ComputedKeys = newResourceConfigShimmedComputedKeys(val, "")
} else {
ret.Config = make(map[string]interface{})
}
ret.Raw = ret.Config
return ret
}
// Record the any config values in ComputedKeys. This field had been unused in
// helper/schema, but in the new protocol we're using this so that the SDK can
// now handle having an unknown collection. The legacy diff code doesn't
// properly handle the unknown, because it can't be expressed in the same way
// between the config and diff.
func newResourceConfigShimmedComputedKeys(val cty.Value, path string) []string {
var ret []string
ty := val.Type()
if val.IsNull() {
return ret
}
if !val.IsKnown() {
// we shouldn't have an entirely unknown resource, but prevent empty
// strings just in case
if len(path) > 0 {
ret = append(ret, path)
}
return ret
}
if path != "" {
path += "."
}
switch {
case ty.IsListType(), ty.IsTupleType(), ty.IsSetType():
i := 0
for it := val.ElementIterator(); it.Next(); i++ {
_, subVal := it.Element()
keys := newResourceConfigShimmedComputedKeys(subVal, fmt.Sprintf("%s%d", path, i))
ret = append(ret, keys...)
}
case ty.IsMapType(), ty.IsObjectType():
for it := val.ElementIterator(); it.Next(); {
subK, subVal := it.Element()
keys := newResourceConfigShimmedComputedKeys(subVal, fmt.Sprintf("%s%s", path, subK.AsString()))
ret = append(ret, keys...)
}
}
return ret
}
// DeepCopy performs a deep copy of the configuration. This makes it safe
// to modify any of the structures that are part of the resource config without
// affecting the original configuration.
func (c *ResourceConfig) DeepCopy() *ResourceConfig {
// DeepCopying a nil should return a nil to avoid panics
if c == nil {
return nil
}
// Copy, this will copy all the exported attributes
copy, err := copystructure.Config{Lock: true}.Copy(c)
if err != nil {
panic(err)
}
// Force the type
result := copy.(*ResourceConfig)
return result
}
// Equal checks the equality of two resource configs.
func (c *ResourceConfig) Equal(c2 *ResourceConfig) bool {
// If either are nil, then they're only equal if they're both nil
if c == nil || c2 == nil {
return c == c2
}
// Sort the computed keys so they're deterministic
sort.Strings(c.ComputedKeys)
sort.Strings(c2.ComputedKeys)
// Two resource configs if their exported properties are equal.
// We don't compare "raw" because it is never used again after
// initialization and for all intents and purposes they are equal
// if the exported properties are equal.
check := [][2]interface{}{
{c.ComputedKeys, c2.ComputedKeys},
{c.Raw, c2.Raw},
{c.Config, c2.Config},
}
for _, pair := range check {
if !reflect.DeepEqual(pair[0], pair[1]) {
return false
}
}
return true
}
// CheckSet checks that the given list of configuration keys is
// properly set. If not, errors are returned for each unset key.
//
// This is useful to be called in the Validate method of a ResourceProvider.
func (c *ResourceConfig) CheckSet(keys []string) []error {
var errs []error
for _, k := range keys {
if !c.IsSet(k) {
errs = append(errs, fmt.Errorf("%s must be set", k))
}
}
return errs
}
// Get looks up a configuration value by key and returns the value.
//
// The second return value is true if the get was successful. Get will
// return the raw value if the key is computed, so you should pair this
// with IsComputed.
func (c *ResourceConfig) Get(k string) (interface{}, bool) {
// We aim to get a value from the configuration. If it is computed,
// then we return the pure raw value.
source := c.Config
if c.IsComputed(k) {
source = c.Raw
}
return c.get(k, source)
}
// GetRaw looks up a configuration value by key and returns the value,
// from the raw, uninterpolated config.
//
// The second return value is true if the get was successful. Get will
// not succeed if the value is being computed.
func (c *ResourceConfig) GetRaw(k string) (interface{}, bool) {
return c.get(k, c.Raw)
}
// IsComputed returns whether the given key is computed or not.
func (c *ResourceConfig) IsComputed(k string) bool {
// The next thing we do is check the config if we get a computed
// value out of it.
v, ok := c.get(k, c.Config)
if !ok {
return false
}
// If value is nil, then it isn't computed
if v == nil {
return false
}
// Test if the value contains an unknown value
var w unknownCheckWalker
if err := reflectwalk.Walk(v, &w); err != nil {
panic(err)
}
return w.Unknown
}
// IsSet checks if the key in the configuration is set. A key is set if
// it has a value or the value is being computed (is unknown currently).
//
// This function should be used rather than checking the keys of the
// raw configuration itself, since a key may be omitted from the raw
// configuration if it is being computed.
func (c *ResourceConfig) IsSet(k string) bool {
if c == nil {
return false
}
if c.IsComputed(k) {
return true
}
if _, ok := c.Get(k); ok {
return true
}
return false
}
func (c *ResourceConfig) get(
k string, raw map[string]interface{}) (interface{}, bool) {
parts := strings.Split(k, ".")
if len(parts) == 1 && parts[0] == "" {
parts = nil
}
var current interface{} = raw
var previous interface{} = nil
for i, part := range parts {
if current == nil {
return nil, false
}
cv := reflect.ValueOf(current)
switch cv.Kind() {
case reflect.Map:
previous = current
v := cv.MapIndex(reflect.ValueOf(part))
if !v.IsValid() {
if i > 0 && i != (len(parts)-1) {
tryKey := strings.Join(parts[i:], ".")
v := cv.MapIndex(reflect.ValueOf(tryKey))
if !v.IsValid() {
return nil, false
}
return v.Interface(), true
}
return nil, false
}
current = v.Interface()
case reflect.Slice:
previous = current
if part == "#" {
// If any value in a list is computed, this whole thing
// is computed and we can't read any part of it.
for i := 0; i < cv.Len(); i++ {
if v := cv.Index(i).Interface(); v == hcl2shim.UnknownVariableValue {
return v, true
}
}
current = cv.Len()
} else {
i, err := strconv.ParseInt(part, 0, 0)
if err != nil {
return nil, false
}
if int(i) < 0 || int(i) >= cv.Len() {
return nil, false
}
current = cv.Index(int(i)).Interface()
}
case reflect.String:
// This happens when map keys contain "." and have a common
// prefix so were split as path components above.
actualKey := strings.Join(parts[i-1:], ".")
if prevMap, ok := previous.(map[string]interface{}); ok {
v, ok := prevMap[actualKey]
return v, ok
}
return nil, false
default:
panic(fmt.Sprintf("Unknown kind: %s", cv.Kind()))
}
}
return current, true
}
// unknownCheckWalker
type unknownCheckWalker struct {
Unknown bool
}
func (w *unknownCheckWalker) Primitive(v reflect.Value) error {
if v.Interface() == hcl2shim.UnknownVariableValue {
w.Unknown = true
}
return nil
}

View File

@ -1,226 +1,5 @@
package terraform
// ResourceProvider is a legacy interface for providers.
//
// This is retained only for compatibility with legacy code. The current
// interface for providers is providers.Interface, in the sibling directory
// named "providers".
type ResourceProvider interface {
/*********************************************************************
* Functions related to the provider
*********************************************************************/
// ProviderSchema returns the config schema for the main provider
// configuration, as would appear in a "provider" block in the
// configuration files.
//
// Currently not all providers support schema. Callers must therefore
// first call Resources and DataSources and ensure that at least one
// resource or data source has the SchemaAvailable flag set.
GetSchema(*ProviderSchemaRequest) (*ProviderSchema, error)
// Input was used prior to v0.12 to ask the provider to prompt the user
// for input to complete the configuration.
//
// From v0.12 onwards this method is never called because Terraform Core
// is able to handle the necessary input logic itself based on the
// schema returned from GetSchema.
Input(UIInput, *ResourceConfig) (*ResourceConfig, error)
// Validate is called once at the beginning with the raw configuration
// (no interpolation done) and can return a list of warnings and/or
// errors.
//
// This is called once with the provider configuration only. It may not
// be called at all if no provider configuration is given.
//
// This should not assume that any values of the configurations are valid.
// The primary use case of this call is to check that required keys are
// set.
Validate(*ResourceConfig) ([]string, []error)
// Configure configures the provider itself with the configuration
// given. This is useful for setting things like access keys.
//
// This won't be called at all if no provider configuration is given.
//
// Configure returns an error if it occurred.
Configure(*ResourceConfig) error
// Resources returns all the available resource types that this provider
// knows how to manage.
Resources() []ResourceType
// Stop is called when the provider should halt any in-flight actions.
//
// This can be used to make a nicer Ctrl-C experience for Terraform.
// Even if this isn't implemented to do anything (just returns nil),
// Terraform will still cleanly stop after the currently executing
// graph node is complete. However, this API can be used to make more
// efficient halts.
//
// Stop doesn't have to and shouldn't block waiting for in-flight actions
// to complete. It should take any action it wants and return immediately
// acknowledging it has received the stop request. Terraform core will
// automatically not make any further API calls to the provider soon
// after Stop is called (technically exactly once the currently executing
// graph nodes are complete).
//
// The error returned, if non-nil, is assumed to mean that signaling the
// stop somehow failed and that the user should expect potentially waiting
// a longer period of time.
Stop() error
/*********************************************************************
* Functions related to individual resources
*********************************************************************/
// ValidateResource is called once at the beginning with the raw
// configuration (no interpolation done) and can return a list of warnings
// and/or errors.
//
// This is called once per resource.
//
// This should not assume any of the values in the resource configuration
// are valid since it is possible they have to be interpolated still.
// The primary use case of this call is to check that the required keys
// are set and that the general structure is correct.
ValidateResource(string, *ResourceConfig) ([]string, []error)
// Apply applies a diff to a specific resource and returns the new
// resource state along with an error.
//
// If the resource state given has an empty ID, then a new resource
// is expected to be created.
Apply(
*InstanceInfo,
*InstanceState,
*InstanceDiff) (*InstanceState, error)
// Diff diffs a resource versus a desired state and returns
// a diff.
Diff(
*InstanceInfo,
*InstanceState,
*ResourceConfig) (*InstanceDiff, error)
// Refresh refreshes a resource and updates all of its attributes
// with the latest information.
Refresh(*InstanceInfo, *InstanceState) (*InstanceState, error)
/*********************************************************************
* Functions related to importing
*********************************************************************/
// ImportState requests that the given resource be imported.
//
// The returned InstanceState only requires ID be set. Importing
// will always call Refresh after the state to complete it.
//
// IMPORTANT: InstanceState doesn't have the resource type attached
// to it. A type must be specified on the state via the Ephemeral
// field on the state.
//
// This function can return multiple states. Normally, an import
// will map 1:1 to a physical resource. However, some resources map
// to multiple. For example, an AWS security group may contain many rules.
// Each rule is represented by a separate resource in Terraform,
// therefore multiple states are returned.
ImportState(*InstanceInfo, string) ([]*InstanceState, error)
/*********************************************************************
* Functions related to data resources
*********************************************************************/
// ValidateDataSource is called once at the beginning with the raw
// configuration (no interpolation done) and can return a list of warnings
// and/or errors.
//
// This is called once per data source instance.
//
// This should not assume any of the values in the resource configuration
// are valid since it is possible they have to be interpolated still.
// The primary use case of this call is to check that the required keys
// are set and that the general structure is correct.
ValidateDataSource(string, *ResourceConfig) ([]string, []error)
// DataSources returns all of the available data sources that this
// provider implements.
DataSources() []DataSource
// ReadDataDiff produces a diff that represents the state that will
// be produced when the given data source is read using a later call
// to ReadDataApply.
ReadDataDiff(*InstanceInfo, *ResourceConfig) (*InstanceDiff, error)
// ReadDataApply initializes a data instance using the configuration
// in a diff produced by ReadDataDiff.
ReadDataApply(*InstanceInfo, *InstanceDiff) (*InstanceState, error)
}
// ResourceProviderCloser is an interface that providers that can close
// connections that aren't needed anymore must implement.
type ResourceProviderCloser interface {
Close() error
}
// ResourceType is a type of resource that a resource provider can manage.
type ResourceType struct {
Name string // Name of the resource, example "instance" (no provider prefix)
Importable bool // Whether this resource supports importing
// SchemaAvailable is set if the provider supports the ProviderSchema,
// ResourceTypeSchema and DataSourceSchema methods. Although it is
// included on each resource type, it's actually a provider-wide setting
// that's smuggled here only because that avoids a breaking change to
// the plugin protocol.
SchemaAvailable bool
}
// DataSource is a data source that a resource provider implements.
type DataSource struct {
Name string
// SchemaAvailable is set if the provider supports the ProviderSchema,
// ResourceTypeSchema and DataSourceSchema methods. Although it is
// included on each resource type, it's actually a provider-wide setting
// that's smuggled here only because that avoids a breaking change to
// the plugin protocol.
SchemaAvailable bool
}
// ResourceProviderFactory is a function type that creates a new instance
// of a resource provider.
type ResourceProviderFactory func() (ResourceProvider, error)
// ResourceProviderFactoryFixed is a helper that creates a
// ResourceProviderFactory that just returns some fixed provider.
func ResourceProviderFactoryFixed(p ResourceProvider) ResourceProviderFactory {
return func() (ResourceProvider, error) {
return p, nil
}
}
func ProviderHasResource(p ResourceProvider, n string) bool {
for _, rt := range p.Resources() {
if rt.Name == n {
return true
}
}
return false
}
func ProviderHasDataSource(p ResourceProvider, n string) bool {
for _, rt := range p.DataSources() {
if rt.Name == n {
return true
}
}
return false
}
const errPluginInit = `
Plugin reinitialization required. Please run "terraform init".

View File

@ -1,315 +0,0 @@
package terraform
import (
"sync"
)
// MockResourceProvider implements ResourceProvider but mocks out all the
// calls for testing purposes.
type MockResourceProvider struct {
sync.Mutex
// Anything you want, in case you need to store extra data with the mock.
Meta interface{}
CloseCalled bool
CloseError error
GetSchemaCalled bool
GetSchemaRequest *ProviderSchemaRequest
GetSchemaReturn *ProviderSchema
GetSchemaReturnError error
InputCalled bool
InputInput UIInput
InputConfig *ResourceConfig
InputReturnConfig *ResourceConfig
InputReturnError error
InputFn func(UIInput, *ResourceConfig) (*ResourceConfig, error)
ApplyCalled bool
ApplyInfo *InstanceInfo
ApplyState *InstanceState
ApplyDiff *InstanceDiff
ApplyFn func(*InstanceInfo, *InstanceState, *InstanceDiff) (*InstanceState, error)
ApplyReturn *InstanceState
ApplyReturnError error
ConfigureCalled bool
ConfigureConfig *ResourceConfig
ConfigureFn func(*ResourceConfig) error
ConfigureReturnError error
DiffCalled bool
DiffInfo *InstanceInfo
DiffState *InstanceState
DiffDesired *ResourceConfig
DiffFn func(*InstanceInfo, *InstanceState, *ResourceConfig) (*InstanceDiff, error)
DiffReturn *InstanceDiff
DiffReturnError error
RefreshCalled bool
RefreshInfo *InstanceInfo
RefreshState *InstanceState
RefreshFn func(*InstanceInfo, *InstanceState) (*InstanceState, error)
RefreshReturn *InstanceState
RefreshReturnError error
ResourcesCalled bool
ResourcesReturn []ResourceType
ReadDataApplyCalled bool
ReadDataApplyInfo *InstanceInfo
ReadDataApplyDiff *InstanceDiff
ReadDataApplyFn func(*InstanceInfo, *InstanceDiff) (*InstanceState, error)
ReadDataApplyReturn *InstanceState
ReadDataApplyReturnError error
ReadDataDiffCalled bool
ReadDataDiffInfo *InstanceInfo
ReadDataDiffDesired *ResourceConfig
ReadDataDiffFn func(*InstanceInfo, *ResourceConfig) (*InstanceDiff, error)
ReadDataDiffReturn *InstanceDiff
ReadDataDiffReturnError error
StopCalled bool
StopFn func() error
StopReturnError error
DataSourcesCalled bool
DataSourcesReturn []DataSource
ValidateCalled bool
ValidateConfig *ResourceConfig
ValidateFn func(*ResourceConfig) ([]string, []error)
ValidateReturnWarns []string
ValidateReturnErrors []error
ValidateResourceFn func(string, *ResourceConfig) ([]string, []error)
ValidateResourceCalled bool
ValidateResourceType string
ValidateResourceConfig *ResourceConfig
ValidateResourceReturnWarns []string
ValidateResourceReturnErrors []error
ValidateDataSourceFn func(string, *ResourceConfig) ([]string, []error)
ValidateDataSourceCalled bool
ValidateDataSourceType string
ValidateDataSourceConfig *ResourceConfig
ValidateDataSourceReturnWarns []string
ValidateDataSourceReturnErrors []error
ImportStateCalled bool
ImportStateInfo *InstanceInfo
ImportStateID string
ImportStateReturn []*InstanceState
ImportStateReturnError error
ImportStateFn func(*InstanceInfo, string) ([]*InstanceState, error)
}
func (p *MockResourceProvider) Close() error {
p.CloseCalled = true
return p.CloseError
}
func (p *MockResourceProvider) GetSchema(req *ProviderSchemaRequest) (*ProviderSchema, error) {
p.Lock()
defer p.Unlock()
p.GetSchemaCalled = true
p.GetSchemaRequest = req
return p.GetSchemaReturn, p.GetSchemaReturnError
}
func (p *MockResourceProvider) Input(
input UIInput, c *ResourceConfig) (*ResourceConfig, error) {
p.Lock()
defer p.Unlock()
p.InputCalled = true
p.InputInput = input
p.InputConfig = c
if p.InputFn != nil {
return p.InputFn(input, c)
}
return p.InputReturnConfig, p.InputReturnError
}
func (p *MockResourceProvider) Validate(c *ResourceConfig) ([]string, []error) {
p.Lock()
defer p.Unlock()
p.ValidateCalled = true
p.ValidateConfig = c
if p.ValidateFn != nil {
return p.ValidateFn(c)
}
return p.ValidateReturnWarns, p.ValidateReturnErrors
}
func (p *MockResourceProvider) ValidateResource(t string, c *ResourceConfig) ([]string, []error) {
p.Lock()
defer p.Unlock()
p.ValidateResourceCalled = true
p.ValidateResourceType = t
p.ValidateResourceConfig = c
if p.ValidateResourceFn != nil {
return p.ValidateResourceFn(t, c)
}
return p.ValidateResourceReturnWarns, p.ValidateResourceReturnErrors
}
func (p *MockResourceProvider) Configure(c *ResourceConfig) error {
p.Lock()
defer p.Unlock()
p.ConfigureCalled = true
p.ConfigureConfig = c
if p.ConfigureFn != nil {
return p.ConfigureFn(c)
}
return p.ConfigureReturnError
}
func (p *MockResourceProvider) Stop() error {
p.Lock()
defer p.Unlock()
p.StopCalled = true
if p.StopFn != nil {
return p.StopFn()
}
return p.StopReturnError
}
func (p *MockResourceProvider) Apply(
info *InstanceInfo,
state *InstanceState,
diff *InstanceDiff) (*InstanceState, error) {
// We only lock while writing data. Reading is fine
p.Lock()
p.ApplyCalled = true
p.ApplyInfo = info
p.ApplyState = state
p.ApplyDiff = diff
p.Unlock()
if p.ApplyFn != nil {
return p.ApplyFn(info, state, diff)
}
return p.ApplyReturn.DeepCopy(), p.ApplyReturnError
}
func (p *MockResourceProvider) Diff(
info *InstanceInfo,
state *InstanceState,
desired *ResourceConfig) (*InstanceDiff, error) {
p.Lock()
defer p.Unlock()
p.DiffCalled = true
p.DiffInfo = info
p.DiffState = state
p.DiffDesired = desired
if p.DiffFn != nil {
return p.DiffFn(info, state, desired)
}
return p.DiffReturn.DeepCopy(), p.DiffReturnError
}
func (p *MockResourceProvider) Refresh(
info *InstanceInfo,
s *InstanceState) (*InstanceState, error) {
p.Lock()
defer p.Unlock()
p.RefreshCalled = true
p.RefreshInfo = info
p.RefreshState = s
if p.RefreshFn != nil {
return p.RefreshFn(info, s)
}
return p.RefreshReturn.DeepCopy(), p.RefreshReturnError
}
func (p *MockResourceProvider) Resources() []ResourceType {
p.Lock()
defer p.Unlock()
p.ResourcesCalled = true
return p.ResourcesReturn
}
func (p *MockResourceProvider) ImportState(info *InstanceInfo, id string) ([]*InstanceState, error) {
p.Lock()
defer p.Unlock()
p.ImportStateCalled = true
p.ImportStateInfo = info
p.ImportStateID = id
if p.ImportStateFn != nil {
return p.ImportStateFn(info, id)
}
var result []*InstanceState
if p.ImportStateReturn != nil {
result = make([]*InstanceState, len(p.ImportStateReturn))
for i, v := range p.ImportStateReturn {
result[i] = v.DeepCopy()
}
}
return result, p.ImportStateReturnError
}
func (p *MockResourceProvider) ValidateDataSource(t string, c *ResourceConfig) ([]string, []error) {
p.Lock()
defer p.Unlock()
p.ValidateDataSourceCalled = true
p.ValidateDataSourceType = t
p.ValidateDataSourceConfig = c
if p.ValidateDataSourceFn != nil {
return p.ValidateDataSourceFn(t, c)
}
return p.ValidateDataSourceReturnWarns, p.ValidateDataSourceReturnErrors
}
func (p *MockResourceProvider) ReadDataDiff(
info *InstanceInfo,
desired *ResourceConfig) (*InstanceDiff, error) {
p.Lock()
defer p.Unlock()
p.ReadDataDiffCalled = true
p.ReadDataDiffInfo = info
p.ReadDataDiffDesired = desired
if p.ReadDataDiffFn != nil {
return p.ReadDataDiffFn(info, desired)
}
return p.ReadDataDiffReturn.DeepCopy(), p.ReadDataDiffReturnError
}
func (p *MockResourceProvider) ReadDataApply(
info *InstanceInfo,
d *InstanceDiff) (*InstanceState, error) {
p.Lock()
defer p.Unlock()
p.ReadDataApplyCalled = true
p.ReadDataApplyInfo = info
p.ReadDataApplyDiff = d
if p.ReadDataApplyFn != nil {
return p.ReadDataApplyFn(info, d)
}
return p.ReadDataApplyReturn.DeepCopy(), p.ReadDataApplyReturnError
}
func (p *MockResourceProvider) DataSources() []DataSource {
p.Lock()
defer p.Unlock()
p.DataSourcesCalled = true
return p.DataSourcesReturn
}

View File

@ -1,19 +1,12 @@
package terraform
import (
"testing"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/providers"
"github.com/zclconf/go-cty/cty"
)
func TestMockResourceProvider_impl(t *testing.T) {
var _ ResourceProvider = new(MockResourceProvider)
var _ ResourceProviderCloser = new(MockResourceProvider)
}
// testProviderComponentFactory creates a componentFactory that contains only
// a single given.
func testProviderComponentFactory(name string, provider providers.Interface) *basicComponentFactory {
@ -62,18 +55,6 @@ func mockProviderWithResourceTypeSchema(name string, schema *configschema.Block)
}
}
// mockProviderWithDataSourceSchema is a test helper to concisely create a mock
// provider with a schema containing a single data source.
func mockProviderWithDataSourceSchema(name string, schema *configschema.Block) *MockResourceProvider {
return &MockResourceProvider{
GetSchemaReturn: &ProviderSchema{
DataSources: map[string]*configschema.Block{
name: schema,
},
},
}
}
// simpleMockProvider returns a MockProvider that is pre-configured
// with schema for its own config, for a resource type called "test_object" and
// for a data source also called "test_object".

View File

@ -1,674 +0,0 @@
package terraform
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/mitchellh/reflectwalk"
)
func TestResourceConfigGet(t *testing.T) {
fooStringSchema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {Type: cty.String, Optional: true},
},
}
fooListSchema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {Type: cty.List(cty.Number), Optional: true},
},
}
cases := []struct {
Config cty.Value
Schema *configschema.Block
Key string
Value interface{}
}{
{
Config: cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
}),
Schema: fooStringSchema,
Key: "foo",
Value: "bar",
},
{
Config: cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.String),
}),
Schema: fooStringSchema,
Key: "foo",
Value: hcl2shim.UnknownVariableValue,
},
{
Config: cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.NumberIntVal(1),
cty.NumberIntVal(2),
cty.NumberIntVal(5),
}),
}),
Schema: fooListSchema,
Key: "foo.0",
Value: 1,
},
{
Config: cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.NumberIntVal(1),
cty.NumberIntVal(2),
cty.NumberIntVal(5),
}),
}),
Schema: fooListSchema,
Key: "foo.5",
Value: nil,
},
{
Config: cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.NumberIntVal(1),
cty.NumberIntVal(2),
cty.NumberIntVal(5),
}),
}),
Schema: fooListSchema,
Key: "foo.-1",
Value: nil,
},
// get from map
{
Config: cty.ObjectVal(map[string]cty.Value{
"mapname": cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"key": cty.NumberIntVal(1),
}),
}),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
},
},
Key: "mapname.0.key",
Value: 1,
},
// get from map with dot in key
{
Config: cty.ObjectVal(map[string]cty.Value{
"mapname": cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"key.name": cty.NumberIntVal(1),
}),
}),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
},
},
Key: "mapname.0.key.name",
Value: 1,
},
// get from map with overlapping key names
{
Config: cty.ObjectVal(map[string]cty.Value{
"mapname": cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"key.name": cty.NumberIntVal(1),
"key.name.2": cty.NumberIntVal(2),
}),
}),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
},
},
Key: "mapname.0.key.name.2",
Value: 2,
},
{
Config: cty.ObjectVal(map[string]cty.Value{
"mapname": cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"key.name": cty.NumberIntVal(1),
"key.name.foo": cty.NumberIntVal(2),
}),
}),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
},
},
Key: "mapname.0.key.name",
Value: 1,
},
{
Config: cty.ObjectVal(map[string]cty.Value{
"mapname": cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"listkey": cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"key": cty.NumberIntVal(3),
}),
}),
}),
}),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"mapname": {Type: cty.List(cty.Map(cty.List(cty.Map(cty.Number)))), Optional: true},
},
},
Key: "mapname.0.listkey.0.key",
Value: 3,
},
}
for i, tc := range cases {
rc := NewResourceConfigShimmed(tc.Config, tc.Schema)
// Test getting a key
t.Run(fmt.Sprintf("get-%d", i), func(t *testing.T) {
v, ok := rc.Get(tc.Key)
if ok && v == nil {
t.Fatal("(nil, true) returned from Get")
}
if !reflect.DeepEqual(v, tc.Value) {
t.Fatalf("%d bad: %#v", i, v)
}
})
// Test copying and equality
t.Run(fmt.Sprintf("copy-and-equal-%d", i), func(t *testing.T) {
copy := rc.DeepCopy()
if !reflect.DeepEqual(copy, rc) {
t.Fatalf("bad:\n\n%#v\n\n%#v", copy, rc)
}
if !copy.Equal(rc) {
t.Fatalf("copy != rc:\n\n%#v\n\n%#v", copy, rc)
}
if !rc.Equal(copy) {
t.Fatalf("rc != copy:\n\n%#v\n\n%#v", copy, rc)
}
})
}
}
func TestResourceConfigDeepCopy_nil(t *testing.T) {
var nilRc *ResourceConfig
actual := nilRc.DeepCopy()
if actual != nil {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceConfigDeepCopy_nilComputed(t *testing.T) {
rc := &ResourceConfig{}
actual := rc.DeepCopy()
if actual.ComputedKeys != nil {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceConfigEqual_nil(t *testing.T) {
var nilRc *ResourceConfig
notNil := NewResourceConfigShimmed(cty.EmptyObjectVal, &configschema.Block{})
if nilRc.Equal(notNil) {
t.Fatal("should not be equal")
}
if notNil.Equal(nilRc) {
t.Fatal("should not be equal")
}
}
func TestResourceConfigEqual_computedKeyOrder(t *testing.T) {
v := cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.String),
})
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {Type: cty.String, Optional: true},
},
}
rc := NewResourceConfigShimmed(v, schema)
rc2 := NewResourceConfigShimmed(v, schema)
// Set the computed keys manually to force ordering to differ
rc.ComputedKeys = []string{"foo", "bar"}
rc2.ComputedKeys = []string{"bar", "foo"}
if !rc.Equal(rc2) {
t.Fatal("should be equal")
}
}
func TestUnknownCheckWalker(t *testing.T) {
cases := []struct {
Name string
Input interface{}
Result bool
}{
{
"primitive",
42,
false,
},
{
"primitive computed",
hcl2shim.UnknownVariableValue,
true,
},
{
"list",
[]interface{}{"foo", hcl2shim.UnknownVariableValue},
true,
},
{
"nested list",
[]interface{}{
"foo",
[]interface{}{hcl2shim.UnknownVariableValue},
},
true,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
var w unknownCheckWalker
if err := reflectwalk.Walk(tc.Input, &w); err != nil {
t.Fatalf("err: %s", err)
}
if w.Unknown != tc.Result {
t.Fatalf("bad: %v", w.Unknown)
}
})
}
}
func TestNewResourceConfigShimmed(t *testing.T) {
for _, tc := range []struct {
Name string
Val cty.Value
Schema *configschema.Block
Expected *ResourceConfig
}{
{
Name: "empty object",
Val: cty.NullVal(cty.EmptyObject),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
Expected: &ResourceConfig{
Raw: map[string]interface{}{},
Config: map[string]interface{}{},
},
},
{
Name: "basic",
Val: cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
Expected: &ResourceConfig{
Raw: map[string]interface{}{
"foo": "bar",
},
Config: map[string]interface{}{
"foo": "bar",
},
},
},
{
Name: "null string",
Val: cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
Expected: &ResourceConfig{
Raw: map[string]interface{}{},
Config: map[string]interface{}{},
},
},
{
Name: "unknown string",
Val: cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.String),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
Expected: &ResourceConfig{
ComputedKeys: []string{"foo"},
Raw: map[string]interface{}{
"foo": hcl2shim.UnknownVariableValue,
},
Config: map[string]interface{}{
"foo": hcl2shim.UnknownVariableValue,
},
},
},
{
Name: "unknown collections",
Val: cty.ObjectVal(map[string]cty.Value{
"bar": cty.UnknownVal(cty.Map(cty.String)),
"baz": cty.UnknownVal(cty.List(cty.String)),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": {
Type: cty.Map(cty.String),
Required: true,
},
"baz": {
Type: cty.List(cty.String),
Optional: true,
},
},
},
Expected: &ResourceConfig{
ComputedKeys: []string{"bar", "baz"},
Raw: map[string]interface{}{
"bar": hcl2shim.UnknownVariableValue,
"baz": hcl2shim.UnknownVariableValue,
},
Config: map[string]interface{}{
"bar": hcl2shim.UnknownVariableValue,
"baz": hcl2shim.UnknownVariableValue,
},
},
},
{
Name: "null collections",
Val: cty.ObjectVal(map[string]cty.Value{
"bar": cty.NullVal(cty.Map(cty.String)),
"baz": cty.NullVal(cty.List(cty.String)),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": {
Type: cty.Map(cty.String),
Required: true,
},
"baz": {
Type: cty.List(cty.String),
Optional: true,
},
},
},
Expected: &ResourceConfig{
Raw: map[string]interface{}{},
Config: map[string]interface{}{},
},
},
{
Name: "unknown blocks",
Val: cty.ObjectVal(map[string]cty.Value{
"bar": cty.UnknownVal(cty.Map(cty.String)),
"baz": cty.UnknownVal(cty.List(cty.String)),
}),
Schema: &configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"bar": {
Block: configschema.Block{},
Nesting: configschema.NestingList,
},
"baz": {
Block: configschema.Block{},
Nesting: configschema.NestingSet,
},
},
},
Expected: &ResourceConfig{
ComputedKeys: []string{"bar", "baz"},
Raw: map[string]interface{}{
"bar": hcl2shim.UnknownVariableValue,
"baz": hcl2shim.UnknownVariableValue,
},
Config: map[string]interface{}{
"bar": hcl2shim.UnknownVariableValue,
"baz": hcl2shim.UnknownVariableValue,
},
},
},
{
Name: "unknown in nested blocks",
Val: cty.ObjectVal(map[string]cty.Value{
"bar": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"baz": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"list": cty.UnknownVal(cty.List(cty.String)),
}),
}),
}),
}),
}),
Schema: &configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"bar": {
Block: configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"baz": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"list": {Type: cty.List(cty.String),
Optional: true,
},
},
},
Nesting: configschema.NestingList,
},
},
},
Nesting: configschema.NestingList,
},
},
},
Expected: &ResourceConfig{
ComputedKeys: []string{"bar.0.baz.0.list"},
Raw: map[string]interface{}{
"bar": []interface{}{map[string]interface{}{
"baz": []interface{}{map[string]interface{}{
"list": "74D93920-ED26-11E3-AC10-0800200C9A66",
}},
}},
},
Config: map[string]interface{}{
"bar": []interface{}{map[string]interface{}{
"baz": []interface{}{map[string]interface{}{
"list": "74D93920-ED26-11E3-AC10-0800200C9A66",
}},
}},
},
},
},
{
Name: "unknown in set",
Val: cty.ObjectVal(map[string]cty.Value{
"bar": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"val": cty.UnknownVal(cty.String),
}),
}),
}),
Schema: &configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"bar": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"val": {
Type: cty.String,
Optional: true,
},
},
},
Nesting: configschema.NestingSet,
},
},
},
Expected: &ResourceConfig{
ComputedKeys: []string{"bar.0.val"},
Raw: map[string]interface{}{
"bar": []interface{}{map[string]interface{}{
"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
}},
},
Config: map[string]interface{}{
"bar": []interface{}{map[string]interface{}{
"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
}},
},
},
},
{
Name: "unknown in attribute sets",
Val: cty.ObjectVal(map[string]cty.Value{
"bar": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"val": cty.UnknownVal(cty.String),
}),
}),
"baz": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"obj": cty.UnknownVal(cty.Object(map[string]cty.Type{
"attr": cty.List(cty.String),
})),
}),
cty.ObjectVal(map[string]cty.Value{
"obj": cty.ObjectVal(map[string]cty.Value{
"attr": cty.UnknownVal(cty.List(cty.String)),
}),
}),
}),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": &configschema.Attribute{
Type: cty.Set(cty.Object(map[string]cty.Type{
"val": cty.String,
})),
},
"baz": &configschema.Attribute{
Type: cty.Set(cty.Object(map[string]cty.Type{
"obj": cty.Object(map[string]cty.Type{
"attr": cty.List(cty.String),
}),
})),
},
},
},
Expected: &ResourceConfig{
ComputedKeys: []string{"bar.0.val", "baz.0.obj.attr", "baz.1.obj"},
Raw: map[string]interface{}{
"bar": []interface{}{map[string]interface{}{
"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
}},
"baz": []interface{}{
map[string]interface{}{
"obj": map[string]interface{}{
"attr": "74D93920-ED26-11E3-AC10-0800200C9A66",
},
},
map[string]interface{}{
"obj": "74D93920-ED26-11E3-AC10-0800200C9A66",
},
},
},
Config: map[string]interface{}{
"bar": []interface{}{map[string]interface{}{
"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
}},
"baz": []interface{}{
map[string]interface{}{
"obj": map[string]interface{}{
"attr": "74D93920-ED26-11E3-AC10-0800200C9A66",
},
},
map[string]interface{}{
"obj": "74D93920-ED26-11E3-AC10-0800200C9A66",
},
},
},
},
},
{
Name: "null blocks",
Val: cty.ObjectVal(map[string]cty.Value{
"bar": cty.NullVal(cty.Map(cty.String)),
"baz": cty.NullVal(cty.List(cty.String)),
}),
Schema: &configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"bar": {
Block: configschema.Block{},
Nesting: configschema.NestingMap,
},
"baz": {
Block: configschema.Block{},
Nesting: configschema.NestingSingle,
},
},
},
Expected: &ResourceConfig{
Raw: map[string]interface{}{},
Config: map[string]interface{}{},
},
},
} {
t.Run(tc.Name, func(*testing.T) {
cfg := NewResourceConfigShimmed(tc.Val, tc.Schema)
if !tc.Expected.Equal(cfg) {
t.Fatalf("expected:\n%#v\ngot:\n%#v", tc.Expected, cfg)
}
})
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,267 +0,0 @@
package terraform
import (
"fmt"
"sort"
)
// StateFilter is responsible for filtering and searching a state.
//
// This is a separate struct from State rather than a method on State
// because StateFilter might create sidecar data structures to optimize
// filtering on the state.
//
// If you change the State, the filter created is invalid and either
// Reset should be called or a new one should be allocated. StateFilter
// will not watch State for changes and do this for you. If you filter after
// changing the State without calling Reset, the behavior is not defined.
type StateFilter struct {
State *State
}
// Filter takes the addresses specified by fs and finds all the matches.
// The values of fs are resource addressing syntax that can be parsed by
// ParseResourceAddress.
func (f *StateFilter) Filter(fs ...string) ([]*StateFilterResult, error) {
// Parse all the addresses
as := make([]*ResourceAddress, len(fs))
for i, v := range fs {
a, err := ParseResourceAddress(v)
if err != nil {
return nil, fmt.Errorf("Error parsing address '%s': %s", v, err)
}
as[i] = a
}
// If we weren't given any filters, then we list all
if len(fs) == 0 {
as = append(as, &ResourceAddress{Index: -1})
}
// Filter each of the address. We keep track of this in a map to
// strip duplicates.
resultSet := make(map[string]*StateFilterResult)
for _, a := range as {
for _, r := range f.filterSingle(a) {
resultSet[r.String()] = r
}
}
// Make the result list
results := make([]*StateFilterResult, 0, len(resultSet))
for _, v := range resultSet {
results = append(results, v)
}
// Sort them and return
sort.Sort(StateFilterResultSlice(results))
return results, nil
}
func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult {
// The slice to keep track of results
var results []*StateFilterResult
// Go through modules first.
modules := make([]*ModuleState, 0, len(f.State.Modules))
for _, m := range f.State.Modules {
if f.relevant(a, m) {
modules = append(modules, m)
// Only add the module to the results if we haven't specified a type.
// We also ignore the root module.
if a.Type == "" && len(m.Path) > 1 {
results = append(results, &StateFilterResult{
Path: m.Path[1:],
Address: (&ResourceAddress{Path: m.Path[1:]}).String(),
Value: m,
})
}
}
}
// With the modules set, go through all the resources within
// the modules to find relevant resources.
for _, m := range modules {
for n, r := range m.Resources {
// The name in the state contains valuable information. Parse.
key, err := ParseResourceStateKey(n)
if err != nil {
// If we get an error parsing, then just ignore it
// out of the state.
continue
}
// Older states and test fixtures often don't contain the
// type directly on the ResourceState. We add this so StateFilter
// is a bit more robust.
if r.Type == "" {
r.Type = key.Type
}
if f.relevant(a, r) {
if a.Name != "" && a.Name != key.Name {
// Name doesn't match
continue
}
if a.Index >= 0 && key.Index != a.Index {
// Index doesn't match
continue
}
if a.Name != "" && a.Name != key.Name {
continue
}
// Build the address for this resource
addr := &ResourceAddress{
Path: m.Path[1:],
Name: key.Name,
Type: key.Type,
Index: key.Index,
}
// Add the resource level result
resourceResult := &StateFilterResult{
Path: addr.Path,
Address: addr.String(),
Value: r,
}
if !a.InstanceTypeSet {
results = append(results, resourceResult)
}
// Add the instances
if r.Primary != nil {
addr.InstanceType = TypePrimary
addr.InstanceTypeSet = false
results = append(results, &StateFilterResult{
Path: addr.Path,
Address: addr.String(),
Parent: resourceResult,
Value: r.Primary,
})
}
for _, instance := range r.Deposed {
if f.relevant(a, instance) {
addr.InstanceType = TypeDeposed
addr.InstanceTypeSet = true
results = append(results, &StateFilterResult{
Path: addr.Path,
Address: addr.String(),
Parent: resourceResult,
Value: instance,
})
}
}
}
}
}
return results
}
// relevant checks for relevance of this address against the given value.
func (f *StateFilter) relevant(addr *ResourceAddress, raw interface{}) bool {
switch v := raw.(type) {
case *ModuleState:
path := v.Path[1:]
if len(addr.Path) > len(path) {
// Longer path in address means there is no way we match.
return false
}
// Check for a prefix match
for i, p := range addr.Path {
if path[i] != p {
// Any mismatches don't match.
return false
}
}
return true
case *ResourceState:
if addr.Type == "" {
// If we have no resource type, then we're interested in all!
return true
}
// If the type doesn't match we fail immediately
if v.Type != addr.Type {
return false
}
return true
default:
// If we don't know about it, let's just say no
return false
}
}
// StateFilterResult is a single result from a filter operation. Filter
// can match multiple things within a state (module, resource, instance, etc.)
// and this unifies that.
type StateFilterResult struct {
// Module path of the result
Path []string
// Address is the address that can be used to reference this exact result.
Address string
// Parent, if non-nil, is a parent of this result. For instances, the
// parent would be a resource. For resources, the parent would be
// a module. For modules, this is currently nil.
Parent *StateFilterResult
// Value is the actual value. This must be type switched on. It can be
// any data structures that `State` can hold: `ModuleState`,
// `ResourceState`, `InstanceState`.
Value interface{}
}
func (r *StateFilterResult) String() string {
return fmt.Sprintf("%T: %s", r.Value, r.Address)
}
func (r *StateFilterResult) sortedType() int {
switch r.Value.(type) {
case *ModuleState:
return 0
case *ResourceState:
return 1
case *InstanceState:
return 2
default:
return 50
}
}
// StateFilterResultSlice is a slice of results that implements
// sort.Interface. The sorting goal is what is most appealing to
// human output.
type StateFilterResultSlice []*StateFilterResult
func (s StateFilterResultSlice) Len() int { return len(s) }
func (s StateFilterResultSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s StateFilterResultSlice) Less(i, j int) bool {
a, b := s[i], s[j]
// if these address contain an index, we want to sort by index rather than name
addrA, errA := ParseResourceAddress(a.Address)
addrB, errB := ParseResourceAddress(b.Address)
if errA == nil && errB == nil && addrA.Name == addrB.Name && addrA.Index != addrB.Index {
return addrA.Index < addrB.Index
}
// If the addresses are different it is just lexographic sorting
if a.Address != b.Address {
return a.Address < b.Address
}
// Addresses are the same, which means it matters on the type
return a.sortedType() < b.sortedType()
}

File diff suppressed because it is too large Load Diff

View File

@ -1,189 +0,0 @@
package terraform
import (
"fmt"
"github.com/mitchellh/copystructure"
)
// upgradeStateV1ToV2 is used to upgrade a V1 state representation
// into a V2 state representation
func upgradeStateV1ToV2(old *stateV1) (*State, error) {
if old == nil {
return nil, nil
}
remote, err := old.Remote.upgradeToV2()
if err != nil {
return nil, fmt.Errorf("Error upgrading State V1: %v", err)
}
modules := make([]*ModuleState, len(old.Modules))
for i, module := range old.Modules {
upgraded, err := module.upgradeToV2()
if err != nil {
return nil, fmt.Errorf("Error upgrading State V1: %v", err)
}
modules[i] = upgraded
}
if len(modules) == 0 {
modules = nil
}
newState := &State{
Version: 2,
Serial: old.Serial,
Remote: remote,
Modules: modules,
}
newState.sort()
newState.init()
return newState, nil
}
func (old *remoteStateV1) upgradeToV2() (*RemoteState, error) {
if old == nil {
return nil, nil
}
config, err := copystructure.Copy(old.Config)
if err != nil {
return nil, fmt.Errorf("Error upgrading RemoteState V1: %v", err)
}
return &RemoteState{
Type: old.Type,
Config: config.(map[string]string),
}, nil
}
func (old *moduleStateV1) upgradeToV2() (*ModuleState, error) {
if old == nil {
return nil, nil
}
pathRaw, err := copystructure.Copy(old.Path)
if err != nil {
return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err)
}
path, ok := pathRaw.([]string)
if !ok {
return nil, fmt.Errorf("Error upgrading ModuleState V1: path is not a list of strings")
}
if len(path) == 0 {
// We found some V1 states with a nil path. Assume root and catch
// duplicate path errors later (as part of Validate).
path = rootModulePath
}
// Outputs needs upgrading to use the new structure
outputs := make(map[string]*OutputState)
for key, output := range old.Outputs {
outputs[key] = &OutputState{
Type: "string",
Value: output,
Sensitive: false,
}
}
resources := make(map[string]*ResourceState)
for key, oldResource := range old.Resources {
upgraded, err := oldResource.upgradeToV2()
if err != nil {
return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err)
}
resources[key] = upgraded
}
dependencies, err := copystructure.Copy(old.Dependencies)
if err != nil {
return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err)
}
return &ModuleState{
Path: path,
Outputs: outputs,
Resources: resources,
Dependencies: dependencies.([]string),
}, nil
}
func (old *resourceStateV1) upgradeToV2() (*ResourceState, error) {
if old == nil {
return nil, nil
}
dependencies, err := copystructure.Copy(old.Dependencies)
if err != nil {
return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err)
}
primary, err := old.Primary.upgradeToV2()
if err != nil {
return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err)
}
deposed := make([]*InstanceState, len(old.Deposed))
for i, v := range old.Deposed {
upgraded, err := v.upgradeToV2()
if err != nil {
return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err)
}
deposed[i] = upgraded
}
if len(deposed) == 0 {
deposed = nil
}
return &ResourceState{
Type: old.Type,
Dependencies: dependencies.([]string),
Primary: primary,
Deposed: deposed,
Provider: old.Provider,
}, nil
}
func (old *instanceStateV1) upgradeToV2() (*InstanceState, error) {
if old == nil {
return nil, nil
}
attributes, err := copystructure.Copy(old.Attributes)
if err != nil {
return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err)
}
ephemeral, err := old.Ephemeral.upgradeToV2()
if err != nil {
return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err)
}
meta, err := copystructure.Copy(old.Meta)
if err != nil {
return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err)
}
newMeta := make(map[string]interface{})
for k, v := range meta.(map[string]string) {
newMeta[k] = v
}
return &InstanceState{
ID: old.ID,
Attributes: attributes.(map[string]string),
Ephemeral: *ephemeral,
Meta: newMeta,
}, nil
}
func (old *ephemeralStateV1) upgradeToV2() (*EphemeralState, error) {
connInfo, err := copystructure.Copy(old.ConnInfo)
if err != nil {
return nil, fmt.Errorf("Error upgrading EphemeralState V1: %v", err)
}
return &EphemeralState{
ConnInfo: connInfo.(map[string]string),
}, nil
}

View File

@ -1,142 +0,0 @@
package terraform
import (
"fmt"
"log"
"regexp"
"sort"
"strconv"
"strings"
)
// The upgrade process from V2 to V3 state does not affect the structure,
// so we do not need to redeclare all of the structs involved - we just
// take a deep copy of the old structure and assert the version number is
// as we expect.
func upgradeStateV2ToV3(old *State) (*State, error) {
new := old.DeepCopy()
// Ensure the copied version is v2 before attempting to upgrade
if new.Version != 2 {
return nil, fmt.Errorf("Cannot apply v2->v3 state upgrade to " +
"a state which is not version 2.")
}
// Set the new version number
new.Version = 3
// Change the counts for things which look like maps to use the %
// syntax. Remove counts for empty collections - they will be added
// back in later.
for _, module := range new.Modules {
for _, resource := range module.Resources {
// Upgrade Primary
if resource.Primary != nil {
upgradeAttributesV2ToV3(resource.Primary)
}
// Upgrade Deposed
if resource.Deposed != nil {
for _, deposed := range resource.Deposed {
upgradeAttributesV2ToV3(deposed)
}
}
}
}
return new, nil
}
func upgradeAttributesV2ToV3(instanceState *InstanceState) error {
collectionKeyRegexp := regexp.MustCompile(`^(.*\.)#$`)
collectionSubkeyRegexp := regexp.MustCompile(`^([^\.]+)\..*`)
// Identify the key prefix of anything which is a collection
var collectionKeyPrefixes []string
for key := range instanceState.Attributes {
if submatches := collectionKeyRegexp.FindAllStringSubmatch(key, -1); len(submatches) > 0 {
collectionKeyPrefixes = append(collectionKeyPrefixes, submatches[0][1])
}
}
sort.Strings(collectionKeyPrefixes)
log.Printf("[STATE UPGRADE] Detected the following collections in state: %v", collectionKeyPrefixes)
// This could be rolled into fewer loops, but it is somewhat clearer this way, and will not
// run very often.
for _, prefix := range collectionKeyPrefixes {
// First get the actual keys that belong to this prefix
var potentialKeysMatching []string
for key := range instanceState.Attributes {
if strings.HasPrefix(key, prefix) {
potentialKeysMatching = append(potentialKeysMatching, strings.TrimPrefix(key, prefix))
}
}
sort.Strings(potentialKeysMatching)
var actualKeysMatching []string
for _, key := range potentialKeysMatching {
if submatches := collectionSubkeyRegexp.FindAllStringSubmatch(key, -1); len(submatches) > 0 {
actualKeysMatching = append(actualKeysMatching, submatches[0][1])
} else {
if key != "#" {
actualKeysMatching = append(actualKeysMatching, key)
}
}
}
actualKeysMatching = uniqueSortedStrings(actualKeysMatching)
// Now inspect the keys in order to determine whether this is most likely to be
// a map, list or set. There is room for error here, so we log in each case. If
// there is no method of telling, we remove the key from the InstanceState in
// order that it will be recreated. Again, this could be rolled into fewer loops
// but we prefer clarity.
oldCountKey := fmt.Sprintf("%s#", prefix)
// First, detect "obvious" maps - which have non-numeric keys (mostly).
hasNonNumericKeys := false
for _, key := range actualKeysMatching {
if _, err := strconv.Atoi(key); err != nil {
hasNonNumericKeys = true
}
}
if hasNonNumericKeys {
newCountKey := fmt.Sprintf("%s%%", prefix)
instanceState.Attributes[newCountKey] = instanceState.Attributes[oldCountKey]
delete(instanceState.Attributes, oldCountKey)
log.Printf("[STATE UPGRADE] Detected %s as a map. Replaced count = %s",
strings.TrimSuffix(prefix, "."), instanceState.Attributes[newCountKey])
}
// Now detect empty collections and remove them from state.
if len(actualKeysMatching) == 0 {
delete(instanceState.Attributes, oldCountKey)
log.Printf("[STATE UPGRADE] Detected %s as an empty collection. Removed from state.",
strings.TrimSuffix(prefix, "."))
}
}
return nil
}
// uniqueSortedStrings removes duplicates from a slice of strings and returns
// a sorted slice of the unique strings.
func uniqueSortedStrings(input []string) []string {
uniquemap := make(map[string]struct{})
for _, str := range input {
uniquemap[str] = struct{}{}
}
output := make([]string, len(uniquemap))
i := 0
for key := range uniquemap {
output[i] = key
i = i + 1
}
sort.Strings(output)
return output
}

View File

@ -1,145 +0,0 @@
package terraform
// stateV1 keeps track of a snapshot state-of-the-world that Terraform
// can use to keep track of what real world resources it is actually
// managing.
//
// stateV1 is _only used for the purposes of backwards compatibility
// and is no longer used in Terraform.
//
// For the upgrade process, see state_upgrade_v1_to_v2.go
type stateV1 struct {
// Version is the protocol version. "1" for a StateV1.
Version int `json:"version"`
// Serial is incremented on any operation that modifies
// the State file. It is used to detect potentially conflicting
// updates.
Serial int64 `json:"serial"`
// Remote is used to track the metadata required to
// pull and push state files from a remote storage endpoint.
Remote *remoteStateV1 `json:"remote,omitempty"`
// Modules contains all the modules in a breadth-first order
Modules []*moduleStateV1 `json:"modules"`
}
type remoteStateV1 struct {
// Type controls the client we use for the remote state
Type string `json:"type"`
// Config is used to store arbitrary configuration that
// is type specific
Config map[string]string `json:"config"`
}
type moduleStateV1 struct {
// Path is the import path from the root module. Modules imports are
// always disjoint, so the path represents amodule tree
Path []string `json:"path"`
// Outputs declared by the module and maintained for each module
// even though only the root module technically needs to be kept.
// This allows operators to inspect values at the boundaries.
Outputs map[string]string `json:"outputs"`
// Resources is a mapping of the logically named resource to
// the state of the resource. Each resource may actually have
// N instances underneath, although a user only needs to think
// about the 1:1 case.
Resources map[string]*resourceStateV1 `json:"resources"`
// Dependencies are a list of things that this module relies on
// existing to remain intact. For example: an module may depend
// on a VPC ID given by an aws_vpc resource.
//
// Terraform uses this information to build valid destruction
// orders and to warn the user if they're destroying a module that
// another resource depends on.
//
// Things can be put into this list that may not be managed by
// Terraform. If Terraform doesn't find a matching ID in the
// overall state, then it assumes it isn't managed and doesn't
// worry about it.
Dependencies []string `json:"depends_on,omitempty"`
}
type resourceStateV1 struct {
// This is filled in and managed by Terraform, and is the resource
// type itself such as "mycloud_instance". If a resource provider sets
// this value, it won't be persisted.
Type string `json:"type"`
// Dependencies are a list of things that this resource relies on
// existing to remain intact. For example: an AWS instance might
// depend on a subnet (which itself might depend on a VPC, and so
// on).
//
// Terraform uses this information to build valid destruction
// orders and to warn the user if they're destroying a resource that
// another resource depends on.
//
// Things can be put into this list that may not be managed by
// Terraform. If Terraform doesn't find a matching ID in the
// overall state, then it assumes it isn't managed and doesn't
// worry about it.
Dependencies []string `json:"depends_on,omitempty"`
// Primary is the current active instance for this resource.
// It can be replaced but only after a successful creation.
// This is the instances on which providers will act.
Primary *instanceStateV1 `json:"primary"`
// Tainted is used to track any underlying instances that
// have been created but are in a bad or unknown state and
// need to be cleaned up subsequently. In the
// standard case, there is only at most a single instance.
// However, in pathological cases, it is possible for the number
// of instances to accumulate.
Tainted []*instanceStateV1 `json:"tainted,omitempty"`
// Deposed is used in the mechanics of CreateBeforeDestroy: the existing
// Primary is Deposed to get it out of the way for the replacement Primary to
// be created by Apply. If the replacement Primary creates successfully, the
// Deposed instance is cleaned up. If there were problems creating the
// replacement, the instance remains in the Deposed list so it can be
// destroyed in a future run. Functionally, Deposed instances are very
// similar to Tainted instances in that Terraform is only tracking them in
// order to remember to destroy them.
Deposed []*instanceStateV1 `json:"deposed,omitempty"`
// Provider is used when a resource is connected to a provider with an alias.
// If this string is empty, the resource is connected to the default provider,
// e.g. "aws_instance" goes with the "aws" provider.
// If the resource block contained a "provider" key, that value will be set here.
Provider string `json:"provider,omitempty"`
}
type instanceStateV1 struct {
// A unique ID for this resource. This is opaque to Terraform
// and is only meant as a lookup mechanism for the providers.
ID string `json:"id"`
// Attributes are basic information about the resource. Any keys here
// are accessible in variable format within Terraform configurations:
// ${resourcetype.name.attribute}.
Attributes map[string]string `json:"attributes,omitempty"`
// Ephemeral is used to store any state associated with this instance
// that is necessary for the Terraform run to complete, but is not
// persisted to a state file.
Ephemeral ephemeralStateV1 `json:"-"`
// Meta is a simple K/V map that is persisted to the State but otherwise
// ignored by Terraform core. It's meant to be used for accounting by
// external client code.
Meta map[string]string `json:"meta,omitempty"`
}
type ephemeralStateV1 struct {
// ConnInfo is used for the providers to export information which is
// used to connect to the resource for provisioning. For example,
// this could contain SSH or WinRM credentials.
ConnInfo map[string]string `json:"-"`
}

View File

@ -1,19 +0,0 @@
package terraform
import (
"os"
"testing"
)
// TestStateFile writes the given state to the path.
func TestStateFile(t *testing.T, path string, state *State) {
f, err := os.Create(path)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
if err := WriteState(state, f); err != nil {
t.Fatalf("err: %s", err)
}
}

View File

@ -68,12 +68,12 @@ func TestReferenceTransformer_path(t *testing.T) {
})
g.Add(&graphNodeRefParentTest{
NameValue: "child.A",
PathValue: []string{"root", "child"},
PathValue: addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "child"}},
Names: []string{"A"},
})
g.Add(&graphNodeRefChildTest{
NameValue: "child.B",
PathValue: []string{"root", "child"},
PathValue: addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "child"}},
Refs: []string{"A"},
})
@ -214,7 +214,7 @@ func TestReferenceMapReferences(t *testing.T) {
type graphNodeRefParentTest struct {
NameValue string
PathValue []string
PathValue addrs.ModuleInstance
Names []string
}
@ -233,16 +233,16 @@ func (n *graphNodeRefParentTest) ReferenceableAddrs() []addrs.Referenceable {
}
func (n *graphNodeRefParentTest) Path() addrs.ModuleInstance {
return normalizeModulePath(n.PathValue)
return n.PathValue
}
func (n *graphNodeRefParentTest) ModulePath() addrs.Module {
return normalizeModulePath(n.PathValue).Module()
return n.PathValue.Module()
}
type graphNodeRefChildTest struct {
NameValue string
PathValue []string
PathValue addrs.ModuleInstance
Refs []string
}
@ -263,11 +263,11 @@ func (n *graphNodeRefChildTest) References() []*addrs.Reference {
}
func (n *graphNodeRefChildTest) Path() addrs.ModuleInstance {
return normalizeModulePath(n.PathValue)
return n.PathValue
}
func (n *graphNodeRefChildTest) ModulePath() addrs.Module {
return normalizeModulePath(n.PathValue).Module()
return n.PathValue.Module()
}
type graphNodeFakeResourceInstance struct {

View File

@ -1,190 +0,0 @@
package terraform
import (
"bytes"
"reflect"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
)
// TestReadUpgradeStateV1toV3 tests the state upgrade process from the V1 state
// to the current version, and needs editing each time. This means it tests the
// entire pipeline of upgrades (which migrate version to version).
func TestReadUpgradeStateV1toV3(t *testing.T) {
// ReadState should transparently detect the old version but will upgrade
// it on Write.
actual, err := ReadState(strings.NewReader(testV1State))
if err != nil {
t.Fatalf("err: %s", err)
}
buf := new(bytes.Buffer)
if err := WriteState(actual, buf); err != nil {
t.Fatalf("err: %s", err)
}
if actual.Version != 3 {
t.Fatalf("bad: State version not incremented; is %d", actual.Version)
}
roundTripped, err := ReadState(buf)
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(actual, roundTripped) {
t.Logf("actual:\n%#v", actual)
t.Fatalf("roundTripped:\n%#v", roundTripped)
}
}
func TestReadUpgradeStateV1toV3_outputs(t *testing.T) {
// ReadState should transparently detect the old version but will upgrade
// it on Write.
actual, err := ReadState(strings.NewReader(testV1StateWithOutputs))
if err != nil {
t.Fatalf("err: %s", err)
}
buf := new(bytes.Buffer)
if err := WriteState(actual, buf); err != nil {
t.Fatalf("err: %s", err)
}
if actual.Version != 3 {
t.Fatalf("bad: State version not incremented; is %d", actual.Version)
}
roundTripped, err := ReadState(buf)
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(actual, roundTripped) {
spew.Config.DisableMethods = true
t.Fatalf("bad:\n%s\n\nround tripped:\n%s\n", spew.Sdump(actual), spew.Sdump(roundTripped))
spew.Config.DisableMethods = false
}
}
// Upgrading the state should not lose empty module Outputs and Resources maps
// during upgrade. The init for a new module initializes new maps, so we may not
// be expecting to check for a nil map.
func TestReadUpgradeStateV1toV3_emptyState(t *testing.T) {
// ReadState should transparently detect the old version but will upgrade
// it on Write.
orig, err := ReadStateV1([]byte(testV1EmptyState))
if err != nil {
t.Fatalf("err: %s", err)
}
stateV2, err := upgradeStateV1ToV2(orig)
if err != nil {
t.Fatalf("error attempting upgradeStateV1ToV2: %s", err)
}
for _, m := range stateV2.Modules {
if m.Resources == nil {
t.Fatal("V1 to V2 upgrade lost module.Resources")
}
if m.Outputs == nil {
t.Fatal("V1 to V2 upgrade lost module.Outputs")
}
}
stateV3, err := upgradeStateV2ToV3(stateV2)
if err != nil {
t.Fatalf("error attempting to upgradeStateV2ToV3: %s", err)
}
for _, m := range stateV3.Modules {
if m.Resources == nil {
t.Fatal("V2 to V3 upgrade lost module.Resources")
}
if m.Outputs == nil {
t.Fatal("V2 to V3 upgrade lost module.Outputs")
}
}
}
const testV1EmptyState = `{
"version": 1,
"serial": 0,
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {}
}
]
}
`
const testV1State = `{
"version": 1,
"serial": 9,
"remote": {
"type": "http",
"config": {
"url": "http://my-cool-server.com/"
}
},
"modules": [
{
"path": [
"root"
],
"outputs": null,
"resources": {
"foo": {
"type": "",
"primary": {
"id": "bar"
}
}
},
"depends_on": [
"aws_instance.bar"
]
}
]
}
`
const testV1StateWithOutputs = `{
"version": 1,
"serial": 9,
"remote": {
"type": "http",
"config": {
"url": "http://my-cool-server.com/"
}
},
"modules": [
{
"path": [
"root"
],
"outputs": {
"foo": "bar",
"baz": "foo"
},
"resources": {
"foo": {
"type": "",
"primary": {
"id": "bar"
}
}
},
"depends_on": [
"aws_instance.bar"
]
}
]
}
`

View File

@ -1,202 +0,0 @@
package terraform
import (
"bytes"
"strings"
"testing"
)
// TestReadUpgradeStateV2toV3 tests the state upgrade process from the V2 state
// to the current version, and needs editing each time. This means it tests the
// entire pipeline of upgrades (which migrate version to version).
func TestReadUpgradeStateV2toV3(t *testing.T) {
// ReadState should transparently detect the old version but will upgrade
// it on Write.
upgraded, err := ReadState(strings.NewReader(testV2State))
if err != nil {
t.Fatalf("err: %s", err)
}
buf := new(bytes.Buffer)
if err := WriteState(upgraded, buf); err != nil {
t.Fatalf("err: %s", err)
}
if upgraded.Version != 3 {
t.Fatalf("bad: State version not incremented; is %d", upgraded.Version)
}
// For this test we cannot assert that we match the round trip because an
// empty map has been removed from state. Instead we make assertions against
// some of the key fields in the _upgraded_ state.
instanceState, ok := upgraded.RootModule().Resources["test_resource.main"]
if !ok {
t.Fatalf("Instance state for test_resource.main was removed from state during upgrade")
}
primary := instanceState.Primary
if primary == nil {
t.Fatalf("Primary instance was removed from state for test_resource.main")
}
// Non-empty computed map is moved from .# to .%
if _, ok := primary.Attributes["computed_map.#"]; ok {
t.Fatalf("Count was not upgraded from .# to .%% for computed_map")
}
if count, ok := primary.Attributes["computed_map.%"]; !ok || count != "1" {
t.Fatalf("Count was not in .%% or was not 2 for computed_map")
}
// list_of_map top level retains .#
if count, ok := primary.Attributes["list_of_map.#"]; !ok || count != "2" {
t.Fatal("Count for list_of_map was migrated incorrectly")
}
// list_of_map.0 is moved from .# to .%
if _, ok := primary.Attributes["list_of_map.0.#"]; ok {
t.Fatalf("Count was not upgraded from .# to .%% for list_of_map.0")
}
if count, ok := primary.Attributes["list_of_map.0.%"]; !ok || count != "2" {
t.Fatalf("Count was not in .%% or was not 2 for list_of_map.0")
}
// list_of_map.1 is moved from .# to .%
if _, ok := primary.Attributes["list_of_map.1.#"]; ok {
t.Fatalf("Count was not upgraded from .# to .%% for list_of_map.1")
}
if count, ok := primary.Attributes["list_of_map.1.%"]; !ok || count != "2" {
t.Fatalf("Count was not in .%% or was not 2 for list_of_map.1")
}
// map is moved from .# to .%
if _, ok := primary.Attributes["map.#"]; ok {
t.Fatalf("Count was not upgraded from .# to .%% for map")
}
if count, ok := primary.Attributes["map.%"]; !ok || count != "2" {
t.Fatalf("Count was not in .%% or was not 2 for map")
}
// optional_computed_map should be removed from state
if _, ok := primary.Attributes["optional_computed_map"]; ok {
t.Fatal("optional_computed_map was not removed from state")
}
// required_map is moved from .# to .%
if _, ok := primary.Attributes["required_map.#"]; ok {
t.Fatalf("Count was not upgraded from .# to .%% for required_map")
}
if count, ok := primary.Attributes["required_map.%"]; !ok || count != "3" {
t.Fatalf("Count was not in .%% or was not 3 for map")
}
// computed_list keeps .#
if count, ok := primary.Attributes["computed_list.#"]; !ok || count != "2" {
t.Fatal("Count was migrated incorrectly for computed_list")
}
// computed_set keeps .#
if count, ok := primary.Attributes["computed_set.#"]; !ok || count != "2" {
t.Fatal("Count was migrated incorrectly for computed_set")
}
if val, ok := primary.Attributes["computed_set.2337322984"]; !ok || val != "setval1" {
t.Fatal("Set item for computed_set.2337322984 changed or moved")
}
if val, ok := primary.Attributes["computed_set.307881554"]; !ok || val != "setval2" {
t.Fatal("Set item for computed_set.307881554 changed or moved")
}
// string properties are unaffected
if val, ok := primary.Attributes["id"]; !ok || val != "testId" {
t.Fatal("id was not set correctly after migration")
}
}
const testV2State = `{
"version": 2,
"terraform_version": "0.7.0",
"serial": 2,
"modules": [
{
"path": [
"root"
],
"outputs": {
"computed_map": {
"sensitive": false,
"type": "map",
"value": {
"key1": "value1"
}
},
"computed_set": {
"sensitive": false,
"type": "list",
"value": [
"setval1",
"setval2"
]
},
"map": {
"sensitive": false,
"type": "map",
"value": {
"key": "test",
"test": "test"
}
},
"set": {
"sensitive": false,
"type": "list",
"value": [
"test1",
"test2"
]
}
},
"resources": {
"test_resource.main": {
"type": "test_resource",
"primary": {
"id": "testId",
"attributes": {
"computed_list.#": "2",
"computed_list.0": "listval1",
"computed_list.1": "listval2",
"computed_map.#": "1",
"computed_map.key1": "value1",
"computed_read_only": "value_from_api",
"computed_read_only_force_new": "value_from_api",
"computed_set.#": "2",
"computed_set.2337322984": "setval1",
"computed_set.307881554": "setval2",
"id": "testId",
"list_of_map.#": "2",
"list_of_map.0.#": "2",
"list_of_map.0.key1": "value1",
"list_of_map.0.key2": "value2",
"list_of_map.1.#": "2",
"list_of_map.1.key3": "value3",
"list_of_map.1.key4": "value4",
"map.#": "2",
"map.key": "test",
"map.test": "test",
"map_that_look_like_set.#": "2",
"map_that_look_like_set.12352223": "hello",
"map_that_look_like_set.36234341": "world",
"optional_computed_map.#": "0",
"required": "Hello World",
"required_map.#": "3",
"required_map.key1": "value1",
"required_map.key2": "value2",
"required_map.key3": "value3",
"set.#": "2",
"set.2326977762": "test1",
"set.331058520": "test2"
}
}
}
}
}
]
}
`