internal/legacy/terraform

This is a partial copy of the terraform package to preserve the legacy
types for internal use.
This commit is contained in:
James Bardin 2020-11-17 18:00:52 -05:00
parent c6ab9b1553
commit a49e7eee8b
41 changed files with 13147 additions and 0 deletions

View File

@ -0,0 +1,65 @@
package terraform
import (
"fmt"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/provisioners"
)
// contextComponentFactory is the interface that Context uses
// to initialize various components such as providers and provisioners.
// This factory gets more information than the raw maps using to initialize
// a Context. This information is used for debugging.
type contextComponentFactory interface {
// ResourceProvider creates a new ResourceProvider with the given type.
ResourceProvider(typ addrs.Provider) (providers.Interface, error)
ResourceProviders() []string
// ResourceProvisioner creates a new ResourceProvisioner with the given
// type.
ResourceProvisioner(typ string) (provisioners.Interface, error)
ResourceProvisioners() []string
}
// basicComponentFactory just calls a factory from a map directly.
type basicComponentFactory struct {
providers map[addrs.Provider]providers.Factory
provisioners map[string]ProvisionerFactory
}
func (c *basicComponentFactory) ResourceProviders() []string {
var result []string
for k := range c.providers {
result = append(result, k.String())
}
return result
}
func (c *basicComponentFactory) ResourceProvisioners() []string {
var result []string
for k := range c.provisioners {
result = append(result, k)
}
return result
}
func (c *basicComponentFactory) ResourceProvider(typ addrs.Provider) (providers.Interface, error) {
f, ok := c.providers[typ]
if !ok {
return nil, fmt.Errorf("unknown provider %q", typ.String())
}
return f()
}
func (c *basicComponentFactory) ResourceProvisioner(typ string) (provisioners.Interface, error) {
f, ok := c.provisioners[typ]
if !ok {
return nil, fmt.Errorf("unknown provisioner %q", typ)
}
return f()
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
package terraform
import "os"
// This file holds feature flags for the next release
var flagWarnOutputErrors = os.Getenv("TF_WARN_OUTPUT_ERRORS") != ""

View File

@ -0,0 +1,13 @@
package terraform
//go:generate go run golang.org/x/tools/cmd/stringer -type=InstanceType instancetype.go
// InstanceType is an enum of the various types of instances store in the State
type InstanceType int
const (
TypeInvalid InstanceType = iota
TypePrimary
TypeTainted
TypeDeposed
)

View File

@ -0,0 +1,26 @@
// Code generated by "stringer -type=InstanceType instancetype.go"; DO NOT EDIT.
package terraform
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[TypeInvalid-0]
_ = x[TypePrimary-1]
_ = x[TypeTainted-2]
_ = x[TypeDeposed-3]
}
const _InstanceType_name = "TypeInvalidTypePrimaryTypeTaintedTypeDeposed"
var _InstanceType_index = [...]uint8{0, 11, 22, 33, 44}
func (i InstanceType) String() string {
if i < 0 || i >= InstanceType(len(_InstanceType_index)-1) {
return "InstanceType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _InstanceType_name[_InstanceType_index[i]:_InstanceType_index[i+1]]
}

View File

@ -0,0 +1,364 @@
package terraform
import (
"encoding/json"
"sync"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/hashicorp/terraform/providers"
)
var _ providers.Interface = (*MockProvider)(nil)
// MockProvider implements providers.Interface but mocks out all the
// calls for testing purposes.
type MockProvider struct {
sync.Mutex
// Anything you want, in case you need to store extra data with the mock.
Meta interface{}
GetSchemaCalled bool
GetSchemaReturn *ProviderSchema // This is using ProviderSchema directly rather than providers.GetSchemaResponse for compatibility with old tests
PrepareProviderConfigCalled bool
PrepareProviderConfigResponse providers.PrepareProviderConfigResponse
PrepareProviderConfigRequest providers.PrepareProviderConfigRequest
PrepareProviderConfigFn func(providers.PrepareProviderConfigRequest) providers.PrepareProviderConfigResponse
ValidateResourceTypeConfigCalled bool
ValidateResourceTypeConfigTypeName string
ValidateResourceTypeConfigResponse providers.ValidateResourceTypeConfigResponse
ValidateResourceTypeConfigRequest providers.ValidateResourceTypeConfigRequest
ValidateResourceTypeConfigFn func(providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse
ValidateDataSourceConfigCalled bool
ValidateDataSourceConfigTypeName string
ValidateDataSourceConfigResponse providers.ValidateDataSourceConfigResponse
ValidateDataSourceConfigRequest providers.ValidateDataSourceConfigRequest
ValidateDataSourceConfigFn func(providers.ValidateDataSourceConfigRequest) providers.ValidateDataSourceConfigResponse
UpgradeResourceStateCalled bool
UpgradeResourceStateTypeName string
UpgradeResourceStateResponse providers.UpgradeResourceStateResponse
UpgradeResourceStateRequest providers.UpgradeResourceStateRequest
UpgradeResourceStateFn func(providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse
ConfigureCalled bool
ConfigureResponse providers.ConfigureResponse
ConfigureRequest providers.ConfigureRequest
ConfigureFn func(providers.ConfigureRequest) providers.ConfigureResponse
StopCalled bool
StopFn func() error
StopResponse error
ReadResourceCalled bool
ReadResourceResponse providers.ReadResourceResponse
ReadResourceRequest providers.ReadResourceRequest
ReadResourceFn func(providers.ReadResourceRequest) providers.ReadResourceResponse
PlanResourceChangeCalled bool
PlanResourceChangeResponse providers.PlanResourceChangeResponse
PlanResourceChangeRequest providers.PlanResourceChangeRequest
PlanResourceChangeFn func(providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse
ApplyResourceChangeCalled bool
ApplyResourceChangeResponse providers.ApplyResourceChangeResponse
ApplyResourceChangeRequest providers.ApplyResourceChangeRequest
ApplyResourceChangeFn func(providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse
ImportResourceStateCalled bool
ImportResourceStateResponse providers.ImportResourceStateResponse
ImportResourceStateRequest providers.ImportResourceStateRequest
ImportResourceStateFn func(providers.ImportResourceStateRequest) providers.ImportResourceStateResponse
// Legacy return type for existing tests, which will be shimmed into an
// ImportResourceStateResponse if set
ImportStateReturn []*InstanceState
ReadDataSourceCalled bool
ReadDataSourceResponse providers.ReadDataSourceResponse
ReadDataSourceRequest providers.ReadDataSourceRequest
ReadDataSourceFn func(providers.ReadDataSourceRequest) providers.ReadDataSourceResponse
CloseCalled bool
CloseError error
}
func (p *MockProvider) GetSchema() providers.GetSchemaResponse {
p.Lock()
defer p.Unlock()
p.GetSchemaCalled = true
return p.getSchema()
}
func (p *MockProvider) getSchema() providers.GetSchemaResponse {
// This version of getSchema doesn't do any locking, so it's suitable to
// call from other methods of this mock as long as they are already
// holding the lock.
ret := providers.GetSchemaResponse{
Provider: providers.Schema{},
DataSources: map[string]providers.Schema{},
ResourceTypes: map[string]providers.Schema{},
}
if p.GetSchemaReturn != nil {
ret.Provider.Block = p.GetSchemaReturn.Provider
ret.ProviderMeta.Block = p.GetSchemaReturn.ProviderMeta
for n, s := range p.GetSchemaReturn.DataSources {
ret.DataSources[n] = providers.Schema{
Block: s,
}
}
for n, s := range p.GetSchemaReturn.ResourceTypes {
ret.ResourceTypes[n] = providers.Schema{
Version: int64(p.GetSchemaReturn.ResourceTypeSchemaVersions[n]),
Block: s,
}
}
}
return ret
}
func (p *MockProvider) PrepareProviderConfig(r providers.PrepareProviderConfigRequest) providers.PrepareProviderConfigResponse {
p.Lock()
defer p.Unlock()
p.PrepareProviderConfigCalled = true
p.PrepareProviderConfigRequest = r
if p.PrepareProviderConfigFn != nil {
return p.PrepareProviderConfigFn(r)
}
p.PrepareProviderConfigResponse.PreparedConfig = r.Config
return p.PrepareProviderConfigResponse
}
func (p *MockProvider) ValidateResourceTypeConfig(r providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse {
p.Lock()
defer p.Unlock()
p.ValidateResourceTypeConfigCalled = true
p.ValidateResourceTypeConfigRequest = r
if p.ValidateResourceTypeConfigFn != nil {
return p.ValidateResourceTypeConfigFn(r)
}
return p.ValidateResourceTypeConfigResponse
}
func (p *MockProvider) ValidateDataSourceConfig(r providers.ValidateDataSourceConfigRequest) providers.ValidateDataSourceConfigResponse {
p.Lock()
defer p.Unlock()
p.ValidateDataSourceConfigCalled = true
p.ValidateDataSourceConfigRequest = r
if p.ValidateDataSourceConfigFn != nil {
return p.ValidateDataSourceConfigFn(r)
}
return p.ValidateDataSourceConfigResponse
}
func (p *MockProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse {
p.Lock()
defer p.Unlock()
schemas := p.getSchema()
schema := schemas.ResourceTypes[r.TypeName]
schemaType := schema.Block.ImpliedType()
p.UpgradeResourceStateCalled = true
p.UpgradeResourceStateRequest = r
if p.UpgradeResourceStateFn != nil {
return p.UpgradeResourceStateFn(r)
}
resp := p.UpgradeResourceStateResponse
if resp.UpgradedState == cty.NilVal {
switch {
case r.RawStateFlatmap != nil:
v, err := hcl2shim.HCL2ValueFromFlatmap(r.RawStateFlatmap, schemaType)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
resp.UpgradedState = v
case len(r.RawStateJSON) > 0:
v, err := ctyjson.Unmarshal(r.RawStateJSON, schemaType)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
resp.UpgradedState = v
}
}
return resp
}
func (p *MockProvider) Configure(r providers.ConfigureRequest) providers.ConfigureResponse {
p.Lock()
defer p.Unlock()
p.ConfigureCalled = true
p.ConfigureRequest = r
if p.ConfigureFn != nil {
return p.ConfigureFn(r)
}
return p.ConfigureResponse
}
func (p *MockProvider) Stop() error {
// We intentionally don't lock in this one because the whole point of this
// method is to be called concurrently with another operation that can
// be cancelled. The provider itself is responsible for handling
// any concurrency concerns in this case.
p.StopCalled = true
if p.StopFn != nil {
return p.StopFn()
}
return p.StopResponse
}
func (p *MockProvider) ReadResource(r providers.ReadResourceRequest) providers.ReadResourceResponse {
p.Lock()
defer p.Unlock()
p.ReadResourceCalled = true
p.ReadResourceRequest = r
if p.ReadResourceFn != nil {
return p.ReadResourceFn(r)
}
resp := p.ReadResourceResponse
if resp.NewState != cty.NilVal {
// make sure the NewState fits the schema
// This isn't always the case for the existing tests
newState, err := p.GetSchemaReturn.ResourceTypes[r.TypeName].CoerceValue(resp.NewState)
if err != nil {
panic(err)
}
resp.NewState = newState
return resp
}
// just return the same state we received
resp.NewState = r.PriorState
return resp
}
func (p *MockProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
p.Lock()
defer p.Unlock()
p.PlanResourceChangeCalled = true
p.PlanResourceChangeRequest = r
if p.PlanResourceChangeFn != nil {
return p.PlanResourceChangeFn(r)
}
return p.PlanResourceChangeResponse
}
func (p *MockProvider) ApplyResourceChange(r providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
p.Lock()
p.ApplyResourceChangeCalled = true
p.ApplyResourceChangeRequest = r
p.Unlock()
if p.ApplyResourceChangeFn != nil {
return p.ApplyResourceChangeFn(r)
}
return p.ApplyResourceChangeResponse
}
func (p *MockProvider) ImportResourceState(r providers.ImportResourceStateRequest) providers.ImportResourceStateResponse {
p.Lock()
defer p.Unlock()
if p.ImportStateReturn != nil {
for _, is := range p.ImportStateReturn {
if is.Attributes == nil {
is.Attributes = make(map[string]string)
}
is.Attributes["id"] = is.ID
typeName := is.Ephemeral.Type
// Use the requested type if the resource has no type of it's own.
// We still return the empty type, which will error, but this prevents a panic.
if typeName == "" {
typeName = r.TypeName
}
schema := p.GetSchemaReturn.ResourceTypes[typeName]
if schema == nil {
panic("no schema found for " + typeName)
}
private, err := json.Marshal(is.Meta)
if err != nil {
panic(err)
}
state, err := hcl2shim.HCL2ValueFromFlatmap(is.Attributes, schema.ImpliedType())
if err != nil {
panic(err)
}
state, err = schema.CoerceValue(state)
if err != nil {
panic(err)
}
p.ImportResourceStateResponse.ImportedResources = append(
p.ImportResourceStateResponse.ImportedResources,
providers.ImportedResource{
TypeName: is.Ephemeral.Type,
State: state,
Private: private,
})
}
}
p.ImportResourceStateCalled = true
p.ImportResourceStateRequest = r
if p.ImportResourceStateFn != nil {
return p.ImportResourceStateFn(r)
}
return p.ImportResourceStateResponse
}
func (p *MockProvider) ReadDataSource(r providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
p.Lock()
defer p.Unlock()
p.ReadDataSourceCalled = true
p.ReadDataSourceRequest = r
if p.ReadDataSourceFn != nil {
return p.ReadDataSourceFn(r)
}
return p.ReadDataSourceResponse
}
func (p *MockProvider) Close() error {
p.CloseCalled = true
return p.CloseError
}

View File

@ -0,0 +1,104 @@
package terraform
import (
"sync"
"github.com/hashicorp/terraform/provisioners"
)
var _ provisioners.Interface = (*MockProvisioner)(nil)
// MockProvisioner implements provisioners.Interface but mocks out all the
// calls for testing purposes.
type MockProvisioner struct {
sync.Mutex
// Anything you want, in case you need to store extra data with the mock.
Meta interface{}
GetSchemaCalled bool
GetSchemaResponse provisioners.GetSchemaResponse
ValidateProvisionerConfigCalled bool
ValidateProvisionerConfigRequest provisioners.ValidateProvisionerConfigRequest
ValidateProvisionerConfigResponse provisioners.ValidateProvisionerConfigResponse
ValidateProvisionerConfigFn func(provisioners.ValidateProvisionerConfigRequest) provisioners.ValidateProvisionerConfigResponse
ProvisionResourceCalled bool
ProvisionResourceRequest provisioners.ProvisionResourceRequest
ProvisionResourceResponse provisioners.ProvisionResourceResponse
ProvisionResourceFn func(provisioners.ProvisionResourceRequest) provisioners.ProvisionResourceResponse
StopCalled bool
StopResponse error
StopFn func() error
CloseCalled bool
CloseResponse error
CloseFn func() error
}
func (p *MockProvisioner) GetSchema() provisioners.GetSchemaResponse {
p.Lock()
defer p.Unlock()
p.GetSchemaCalled = true
return p.getSchema()
}
// getSchema is the implementation of GetSchema, which can be called from other
// methods on MockProvisioner that may already be holding the lock.
func (p *MockProvisioner) getSchema() provisioners.GetSchemaResponse {
return p.GetSchemaResponse
}
func (p *MockProvisioner) ValidateProvisionerConfig(r provisioners.ValidateProvisionerConfigRequest) provisioners.ValidateProvisionerConfigResponse {
p.Lock()
defer p.Unlock()
p.ValidateProvisionerConfigCalled = true
p.ValidateProvisionerConfigRequest = r
if p.ValidateProvisionerConfigFn != nil {
return p.ValidateProvisionerConfigFn(r)
}
return p.ValidateProvisionerConfigResponse
}
func (p *MockProvisioner) ProvisionResource(r provisioners.ProvisionResourceRequest) provisioners.ProvisionResourceResponse {
p.Lock()
defer p.Unlock()
p.ProvisionResourceCalled = true
p.ProvisionResourceRequest = r
if p.ProvisionResourceFn != nil {
fn := p.ProvisionResourceFn
return fn(r)
}
return p.ProvisionResourceResponse
}
func (p *MockProvisioner) Stop() error {
// We intentionally don't lock in this one because the whole point of this
// method is to be called concurrently with another operation that can
// be cancelled. The provisioner itself is responsible for handling
// any concurrency concerns in this case.
p.StopCalled = true
if p.StopFn != nil {
return p.StopFn()
}
return p.StopResponse
}
func (p *MockProvisioner) Close() error {
p.Lock()
defer p.Unlock()
p.CloseCalled = true
if p.CloseFn != nil {
return p.CloseFn()
}
return p.CloseResponse
}

View File

@ -0,0 +1,516 @@
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

@ -0,0 +1,618 @@
package terraform
import (
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
)
// ResourceAddress is a way of identifying an individual resource (or,
// eventually, a subset of resources) within the state. It is used for Targets.
type ResourceAddress struct {
// Addresses a resource falling somewhere in the module path
// When specified alone, addresses all resources within a module path
Path []string
// Addresses a specific resource that occurs in a list
Index int
InstanceType InstanceType
InstanceTypeSet bool
Name string
Type string
Mode ResourceMode // significant only if InstanceTypeSet
}
// Copy returns a copy of this ResourceAddress
func (r *ResourceAddress) Copy() *ResourceAddress {
if r == nil {
return nil
}
n := &ResourceAddress{
Path: make([]string, 0, len(r.Path)),
Index: r.Index,
InstanceType: r.InstanceType,
Name: r.Name,
Type: r.Type,
Mode: r.Mode,
}
n.Path = append(n.Path, r.Path...)
return n
}
// String outputs the address that parses into this address.
func (r *ResourceAddress) String() string {
var result []string
for _, p := range r.Path {
result = append(result, "module", p)
}
switch r.Mode {
case ManagedResourceMode:
// nothing to do
case DataResourceMode:
result = append(result, "data")
default:
panic(fmt.Errorf("unsupported resource mode %s", r.Mode))
}
if r.Type != "" {
result = append(result, r.Type)
}
if r.Name != "" {
name := r.Name
if r.InstanceTypeSet {
switch r.InstanceType {
case TypePrimary:
name += ".primary"
case TypeDeposed:
name += ".deposed"
case TypeTainted:
name += ".tainted"
}
}
if r.Index >= 0 {
name += fmt.Sprintf("[%d]", r.Index)
}
result = append(result, name)
}
return strings.Join(result, ".")
}
// HasResourceSpec returns true if the address has a resource spec, as
// defined in the documentation:
// https://www.terraform.io/docs/internals/resource-addressing.html
// In particular, this returns false if the address contains only
// a module path, thus addressing the entire module.
func (r *ResourceAddress) HasResourceSpec() bool {
return r.Type != "" && r.Name != ""
}
// WholeModuleAddress returns the resource address that refers to all
// resources in the same module as the receiver address.
func (r *ResourceAddress) WholeModuleAddress() *ResourceAddress {
return &ResourceAddress{
Path: r.Path,
Index: -1,
InstanceTypeSet: false,
}
}
// MatchesResourceConfig returns true if the receiver matches the given
// configuration resource within the given _static_ module path. Note that
// the module path in a resource address is a _dynamic_ module path, and
// multiple dynamic resource paths may map to a single static path if
// count and for_each are in use on module calls.
//
// Since resource configuration blocks represent all of the instances of
// a multi-instance resource, the index of the address (if any) is not
// considered.
func (r *ResourceAddress) MatchesResourceConfig(path addrs.Module, rc *configs.Resource) bool {
if r.HasResourceSpec() {
// FIXME: Some ugliness while we are between worlds. Functionality
// in "addrs" should eventually replace this ResourceAddress idea
// completely, but for now we'll need to translate to the old
// way of representing resource modes.
switch r.Mode {
case ManagedResourceMode:
if rc.Mode != addrs.ManagedResourceMode {
return false
}
case DataResourceMode:
if rc.Mode != addrs.DataResourceMode {
return false
}
}
if r.Type != rc.Type || r.Name != rc.Name {
return false
}
}
addrPath := r.Path
// normalize
if len(addrPath) == 0 {
addrPath = nil
}
if len(path) == 0 {
path = nil
}
rawPath := []string(path)
return reflect.DeepEqual(addrPath, rawPath)
}
// stateId returns the ID that this resource should be entered with
// in the state. This is also used for diffs. In the future, we'd like to
// move away from this string field so I don't export this.
func (r *ResourceAddress) stateId() string {
result := fmt.Sprintf("%s.%s", r.Type, r.Name)
switch r.Mode {
case ManagedResourceMode:
// Done
case DataResourceMode:
result = fmt.Sprintf("data.%s", result)
default:
panic(fmt.Errorf("unknown resource mode: %s", r.Mode))
}
if r.Index >= 0 {
result += fmt.Sprintf(".%d", r.Index)
}
return result
}
// parseResourceAddressInternal parses the somewhat bespoke resource
// identifier used in states and diffs, such as "instance.name.0".
func parseResourceAddressInternal(s string) (*ResourceAddress, error) {
// Split based on ".". Every resource address should have at least two
// elements (type and name).
parts := strings.Split(s, ".")
if len(parts) < 2 || len(parts) > 4 {
return nil, fmt.Errorf("Invalid internal resource address format: %s", s)
}
// Data resource if we have at least 3 parts and the first one is data
mode := ManagedResourceMode
if len(parts) > 2 && parts[0] == "data" {
mode = DataResourceMode
parts = parts[1:]
}
// If we're not a data resource and we have more than 3, then it is an error
if len(parts) > 3 && mode != DataResourceMode {
return nil, fmt.Errorf("Invalid internal resource address format: %s", s)
}
// Build the parts of the resource address that are guaranteed to exist
addr := &ResourceAddress{
Type: parts[0],
Name: parts[1],
Index: -1,
InstanceType: TypePrimary,
Mode: mode,
}
// If we have more parts, then we have an index. Parse that.
if len(parts) > 2 {
idx, err := strconv.ParseInt(parts[2], 0, 0)
if err != nil {
return nil, fmt.Errorf("Error parsing resource address %q: %s", s, err)
}
addr.Index = int(idx)
}
return addr, nil
}
func ParseResourceAddress(s string) (*ResourceAddress, error) {
matches, err := tokenizeResourceAddress(s)
if err != nil {
return nil, err
}
mode := ManagedResourceMode
if matches["data_prefix"] != "" {
mode = DataResourceMode
}
resourceIndex, err := ParseResourceIndex(matches["index"])
if err != nil {
return nil, err
}
instanceType, err := ParseInstanceType(matches["instance_type"])
if err != nil {
return nil, err
}
path := ParseResourcePath(matches["path"])
// not allowed to say "data." without a type following
if mode == DataResourceMode && matches["type"] == "" {
return nil, fmt.Errorf(
"invalid resource address %q: must target specific data instance",
s,
)
}
return &ResourceAddress{
Path: path,
Index: resourceIndex,
InstanceType: instanceType,
InstanceTypeSet: matches["instance_type"] != "",
Name: matches["name"],
Type: matches["type"],
Mode: mode,
}, nil
}
// ParseResourceAddressForInstanceDiff creates a ResourceAddress for a
// resource name as described in a module diff.
//
// For historical reasons a different addressing format is used in this
// context. The internal format should not be shown in the UI and instead
// this function should be used to translate to a ResourceAddress and
// then, where appropriate, use the String method to produce a canonical
// resource address string for display in the UI.
//
// The given path slice must be empty (or nil) for the root module, and
// otherwise consist of a sequence of module names traversing down into
// the module tree. If a non-nil path is provided, the caller must not
// modify its underlying array after passing it to this function.
func ParseResourceAddressForInstanceDiff(path []string, key string) (*ResourceAddress, error) {
addr, err := parseResourceAddressInternal(key)
if err != nil {
return nil, err
}
addr.Path = path
return addr, nil
}
// NewLegacyResourceAddress creates a ResourceAddress from a new-style
// addrs.AbsResource value.
//
// This is provided for shimming purposes so that we can still easily call into
// older functions that expect the ResourceAddress type.
func NewLegacyResourceAddress(addr addrs.AbsResource) *ResourceAddress {
ret := &ResourceAddress{
Type: addr.Resource.Type,
Name: addr.Resource.Name,
}
switch addr.Resource.Mode {
case addrs.ManagedResourceMode:
ret.Mode = ManagedResourceMode
case addrs.DataResourceMode:
ret.Mode = DataResourceMode
default:
panic(fmt.Errorf("cannot shim %s to legacy ResourceMode value", addr.Resource.Mode))
}
path := make([]string, len(addr.Module))
for i, step := range addr.Module {
if step.InstanceKey != addrs.NoKey {
// At the time of writing this can't happen because we don't
// ket generate keyed module instances. This legacy codepath must
// be removed before we can support "count" and "for_each" for
// modules.
panic(fmt.Errorf("cannot shim module instance step with key %#v to legacy ResourceAddress.Path", step.InstanceKey))
}
path[i] = step.Name
}
ret.Path = path
ret.Index = -1
return ret
}
// NewLegacyResourceInstanceAddress creates a ResourceAddress from a new-style
// addrs.AbsResource value.
//
// This is provided for shimming purposes so that we can still easily call into
// older functions that expect the ResourceAddress type.
func NewLegacyResourceInstanceAddress(addr addrs.AbsResourceInstance) *ResourceAddress {
ret := &ResourceAddress{
Type: addr.Resource.Resource.Type,
Name: addr.Resource.Resource.Name,
}
switch addr.Resource.Resource.Mode {
case addrs.ManagedResourceMode:
ret.Mode = ManagedResourceMode
case addrs.DataResourceMode:
ret.Mode = DataResourceMode
default:
panic(fmt.Errorf("cannot shim %s to legacy ResourceMode value", addr.Resource.Resource.Mode))
}
path := make([]string, len(addr.Module))
for i, step := range addr.Module {
if step.InstanceKey != addrs.NoKey {
// At the time of writing this can't happen because we don't
// ket generate keyed module instances. This legacy codepath must
// be removed before we can support "count" and "for_each" for
// modules.
panic(fmt.Errorf("cannot shim module instance step with key %#v to legacy ResourceAddress.Path", step.InstanceKey))
}
path[i] = step.Name
}
ret.Path = path
if addr.Resource.Key == addrs.NoKey {
ret.Index = -1
} else if ik, ok := addr.Resource.Key.(addrs.IntKey); ok {
ret.Index = int(ik)
} else if _, ok := addr.Resource.Key.(addrs.StringKey); ok {
ret.Index = -1
} else {
panic(fmt.Errorf("cannot shim resource instance with key %#v to legacy ResourceAddress.Index", addr.Resource.Key))
}
return ret
}
// AbsResourceInstanceAddr converts the receiver, a legacy resource address, to
// the new resource address type addrs.AbsResourceInstance.
//
// This method can be used only on an address that has a resource specification.
// It will panic if called on a module-path-only ResourceAddress. Use
// method HasResourceSpec to check before calling, in contexts where it is
// unclear.
//
// addrs.AbsResourceInstance does not represent the "tainted" and "deposed"
// states, and so if these are present on the receiver then they are discarded.
//
// This is provided for shimming purposes so that we can easily adapt functions
// that are returning the legacy ResourceAddress type, for situations where
// the new type is required.
func (addr *ResourceAddress) AbsResourceInstanceAddr() addrs.AbsResourceInstance {
if !addr.HasResourceSpec() {
panic("AbsResourceInstanceAddr called on ResourceAddress with no resource spec")
}
ret := addrs.AbsResourceInstance{
Module: addr.ModuleInstanceAddr(),
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Type: addr.Type,
Name: addr.Name,
},
},
}
switch addr.Mode {
case ManagedResourceMode:
ret.Resource.Resource.Mode = addrs.ManagedResourceMode
case DataResourceMode:
ret.Resource.Resource.Mode = addrs.DataResourceMode
default:
panic(fmt.Errorf("cannot shim %s to addrs.ResourceMode value", addr.Mode))
}
if addr.Index != -1 {
ret.Resource.Key = addrs.IntKey(addr.Index)
}
return ret
}
// ModuleInstanceAddr returns the module path portion of the receiver as a
// addrs.ModuleInstance value.
func (addr *ResourceAddress) ModuleInstanceAddr() addrs.ModuleInstance {
path := make(addrs.ModuleInstance, len(addr.Path))
for i, name := range addr.Path {
path[i] = addrs.ModuleInstanceStep{Name: name}
}
return path
}
// Contains returns true if and only if the given node is contained within
// the receiver.
//
// Containment is defined in terms of the module and resource heirarchy:
// a resource is contained within its module and any ancestor modules,
// an indexed resource instance is contained with the unindexed resource, etc.
func (addr *ResourceAddress) Contains(other *ResourceAddress) bool {
ourPath := addr.Path
givenPath := other.Path
if len(givenPath) < len(ourPath) {
return false
}
for i := range ourPath {
if ourPath[i] != givenPath[i] {
return false
}
}
// If the receiver is a whole-module address then the path prefix
// matching is all we need.
if !addr.HasResourceSpec() {
return true
}
if addr.Type != other.Type || addr.Name != other.Name || addr.Mode != other.Mode {
return false
}
if addr.Index != -1 && addr.Index != other.Index {
return false
}
if addr.InstanceTypeSet && (addr.InstanceTypeSet != other.InstanceTypeSet || addr.InstanceType != other.InstanceType) {
return false
}
return true
}
// Equals returns true if the receiver matches the given address.
//
// The name of this method is a misnomer, since it doesn't test for exact
// equality. Instead, it tests that the _specified_ parts of each
// address match, treating any unspecified parts as wildcards.
//
// See also Contains, which takes a more hierarchical approach to comparing
// addresses.
func (addr *ResourceAddress) Equals(raw interface{}) bool {
other, ok := raw.(*ResourceAddress)
if !ok {
return false
}
pathMatch := len(addr.Path) == 0 && len(other.Path) == 0 ||
reflect.DeepEqual(addr.Path, other.Path)
indexMatch := addr.Index == -1 ||
other.Index == -1 ||
addr.Index == other.Index
nameMatch := addr.Name == "" ||
other.Name == "" ||
addr.Name == other.Name
typeMatch := addr.Type == "" ||
other.Type == "" ||
addr.Type == other.Type
// mode is significant only when type is set
modeMatch := addr.Type == "" ||
other.Type == "" ||
addr.Mode == other.Mode
return pathMatch &&
indexMatch &&
addr.InstanceType == other.InstanceType &&
nameMatch &&
typeMatch &&
modeMatch
}
// Less returns true if and only if the receiver should be sorted before
// the given address when presenting a list of resource addresses to
// an end-user.
//
// This sort uses lexicographic sorting for most components, but uses
// numeric sort for indices, thus causing index 10 to sort after
// index 9, rather than after index 1.
func (addr *ResourceAddress) Less(other *ResourceAddress) bool {
switch {
case len(addr.Path) != len(other.Path):
return len(addr.Path) < len(other.Path)
case !reflect.DeepEqual(addr.Path, other.Path):
// If the two paths are the same length but don't match, we'll just
// cheat and compare the string forms since it's easier than
// comparing all of the path segments in turn, and lexicographic
// comparison is correct for the module path portion.
addrStr := addr.String()
otherStr := other.String()
return addrStr < otherStr
case addr.Mode != other.Mode:
return addr.Mode == DataResourceMode
case addr.Type != other.Type:
return addr.Type < other.Type
case addr.Name != other.Name:
return addr.Name < other.Name
case addr.Index != other.Index:
// Since "Index" is -1 for an un-indexed address, this also conveniently
// sorts unindexed addresses before indexed ones, should they both
// appear for some reason.
return addr.Index < other.Index
case addr.InstanceTypeSet != other.InstanceTypeSet:
return !addr.InstanceTypeSet
case addr.InstanceType != other.InstanceType:
// InstanceType is actually an enum, so this is just an arbitrary
// sort based on the enum numeric values, and thus not particularly
// meaningful.
return addr.InstanceType < other.InstanceType
default:
return false
}
}
func ParseResourceIndex(s string) (int, error) {
if s == "" {
return -1, nil
}
return strconv.Atoi(s)
}
func ParseResourcePath(s string) []string {
if s == "" {
return nil
}
parts := strings.Split(s, ".")
path := make([]string, 0, len(parts))
for _, s := range parts {
// Due to the limitations of the regexp match below, the path match has
// some noise in it we have to filter out :|
if s == "" || s == "module" {
continue
}
path = append(path, s)
}
return path
}
func ParseInstanceType(s string) (InstanceType, error) {
switch s {
case "", "primary":
return TypePrimary, nil
case "deposed":
return TypeDeposed, nil
case "tainted":
return TypeTainted, nil
default:
return TypeInvalid, fmt.Errorf("Unexpected value for InstanceType field: %q", s)
}
}
func tokenizeResourceAddress(s string) (map[string]string, error) {
// Example of portions of the regexp below using the
// string "aws_instance.web.tainted[1]"
re := regexp.MustCompile(`\A` +
// "module.foo.module.bar" (optional)
`(?P<path>(?:module\.(?P<module_name>[^.]+)\.?)*)` +
// possibly "data.", if targeting is a data resource
`(?P<data_prefix>(?:data\.)?)` +
// "aws_instance.web" (optional when module path specified)
`(?:(?P<type>[^.]+)\.(?P<name>[^.[]+))?` +
// "tainted" (optional, omission implies: "primary")
`(?:\.(?P<instance_type>\w+))?` +
// "1" (optional, omission implies: "0")
`(?:\[(?P<index>\d+)\])?` +
`\z`)
groupNames := re.SubexpNames()
rawMatches := re.FindAllStringSubmatch(s, -1)
if len(rawMatches) != 1 {
return nil, fmt.Errorf("invalid resource address %q", s)
}
matches := make(map[string]string)
for i, m := range rawMatches[0] {
matches[groupNames[i]] = m
}
return matches, nil
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
package terraform
//go:generate go run golang.org/x/tools/cmd/stringer -type=ResourceMode -output=resource_mode_string.go resource_mode.go
// ResourceMode is deprecated, use addrs.ResourceMode instead.
// It has been preserved for backwards compatibility.
type ResourceMode int
const (
ManagedResourceMode ResourceMode = iota
DataResourceMode
)

View File

@ -0,0 +1,24 @@
// Code generated by "stringer -type=ResourceMode -output=resource_mode_string.go resource_mode.go"; DO NOT EDIT.
package terraform
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[ManagedResourceMode-0]
_ = x[DataResourceMode-1]
}
const _ResourceMode_name = "ManagedResourceModeDataResourceMode"
var _ResourceMode_index = [...]uint8{0, 19, 35}
func (i ResourceMode) String() string {
if i < 0 || i >= ResourceMode(len(_ResourceMode_index)-1) {
return "ResourceMode(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ResourceMode_name[_ResourceMode_index[i]:_ResourceMode_index[i+1]]
}

View File

@ -0,0 +1,236 @@
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".
Plugins are external binaries that Terraform uses to access and manipulate
resources. The configuration provided requires plugins which can't be located,
don't satisfy the version constraints, or are otherwise incompatible.
Terraform automatically discovers provider requirements from your
configuration, including providers used in child modules. To see the
requirements and constraints, run "terraform providers".
%s
`

View File

@ -0,0 +1,315 @@
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

@ -0,0 +1,69 @@
package terraform
import (
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/provisioners"
)
// ResourceProvisioner is an interface that must be implemented by any
// resource provisioner: the thing that initializes resources in
// a Terraform configuration.
type ResourceProvisioner interface {
// GetConfigSchema returns the schema for the provisioner type's main
// configuration block. This is called prior to Validate to enable some
// basic structural validation to be performed automatically and to allow
// the configuration to be properly extracted from potentially-ambiguous
// configuration file formats.
GetConfigSchema() (*configschema.Block, 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 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.
Validate(*ResourceConfig) ([]string, []error)
// Apply runs the provisioner on a specific resource and returns an error.
// Instead of a diff, the ResourceConfig is provided since provisioners
// only run after a resource has been newly created.
Apply(UIOutput, *InstanceState, *ResourceConfig) error
// Stop is called when the provisioner 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
}
// ResourceProvisionerCloser is an interface that provisioners that can close
// connections that aren't needed anymore must implement.
type ResourceProvisionerCloser interface {
Close() error
}
// ResourceProvisionerFactory is a function type that creates a new instance
// of a resource provisioner.
type ResourceProvisionerFactory func() (ResourceProvisioner, error)
// ProvisionerFactory is a function type that creates a new instance
// of a provisioners.Interface.
type ProvisionerFactory = provisioners.Factory

View File

@ -0,0 +1,87 @@
package terraform
import (
"sync"
"github.com/hashicorp/terraform/configs/configschema"
)
// MockResourceProvisioner implements ResourceProvisioner but mocks out all the
// calls for testing purposes.
type MockResourceProvisioner struct {
sync.Mutex
// Anything you want, in case you need to store extra data with the mock.
Meta interface{}
GetConfigSchemaCalled bool
GetConfigSchemaReturnSchema *configschema.Block
GetConfigSchemaReturnError error
ApplyCalled bool
ApplyOutput UIOutput
ApplyState *InstanceState
ApplyConfig *ResourceConfig
ApplyFn func(*InstanceState, *ResourceConfig) error
ApplyReturnError error
ValidateCalled bool
ValidateConfig *ResourceConfig
ValidateFn func(c *ResourceConfig) ([]string, []error)
ValidateReturnWarns []string
ValidateReturnErrors []error
StopCalled bool
StopFn func() error
StopReturnError error
}
var _ ResourceProvisioner = (*MockResourceProvisioner)(nil)
func (p *MockResourceProvisioner) GetConfigSchema() (*configschema.Block, error) {
p.GetConfigSchemaCalled = true
return p.GetConfigSchemaReturnSchema, p.GetConfigSchemaReturnError
}
func (p *MockResourceProvisioner) 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 *MockResourceProvisioner) Apply(
output UIOutput,
state *InstanceState,
c *ResourceConfig) error {
p.Lock()
p.ApplyCalled = true
p.ApplyOutput = output
p.ApplyState = state
p.ApplyConfig = c
if p.ApplyFn != nil {
fn := p.ApplyFn
p.Unlock()
return fn(state, c)
}
defer p.Unlock()
return p.ApplyReturnError
}
func (p *MockResourceProvisioner) Stop() error {
p.Lock()
defer p.Unlock()
p.StopCalled = true
if p.StopFn != nil {
return p.StopFn()
}
return p.StopReturnError
}

View File

@ -0,0 +1,674 @@
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)
}
})
}
}

View File

@ -0,0 +1,285 @@
package terraform
import (
"fmt"
"log"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/tfdiags"
)
// Schemas is a container for various kinds of schema that Terraform needs
// during processing.
type Schemas struct {
Providers map[addrs.Provider]*ProviderSchema
Provisioners map[string]*configschema.Block
}
// ProviderSchema returns the entire ProviderSchema object that was produced
// by the plugin for the given provider, or nil if no such schema is available.
//
// It's usually better to go use the more precise methods offered by type
// Schemas to handle this detail automatically.
func (ss *Schemas) ProviderSchema(provider addrs.Provider) *ProviderSchema {
if ss.Providers == nil {
return nil
}
return ss.Providers[provider]
}
// ProviderConfig returns the schema for the provider configuration of the
// given provider type, or nil if no such schema is available.
func (ss *Schemas) ProviderConfig(provider addrs.Provider) *configschema.Block {
ps := ss.ProviderSchema(provider)
if ps == nil {
return nil
}
return ps.Provider
}
// ResourceTypeConfig returns the schema for the configuration of a given
// resource type belonging to a given provider type, or nil of no such
// schema is available.
//
// In many cases the provider type is inferrable from the resource type name,
// but this is not always true because users can override the provider for
// a resource using the "provider" meta-argument. Therefore it's important to
// always pass the correct provider name, even though it many cases it feels
// redundant.
func (ss *Schemas) ResourceTypeConfig(provider addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) (block *configschema.Block, schemaVersion uint64) {
ps := ss.ProviderSchema(provider)
if ps == nil || ps.ResourceTypes == nil {
return nil, 0
}
return ps.SchemaForResourceType(resourceMode, resourceType)
}
// ProvisionerConfig returns the schema for the configuration of a given
// provisioner, or nil of no such schema is available.
func (ss *Schemas) ProvisionerConfig(name string) *configschema.Block {
return ss.Provisioners[name]
}
// LoadSchemas searches the given configuration, state and plan (any of which
// may be nil) for constructs that have an associated schema, requests the
// necessary schemas from the given component factory (which must _not_ be nil),
// and returns a single object representing all of the necessary schemas.
//
// If an error is returned, it may be a wrapped tfdiags.Diagnostics describing
// errors across multiple separate objects. Errors here will usually indicate
// either misbehavior on the part of one of the providers or of the provider
// protocol itself. When returned with errors, the returned schemas object is
// still valid but may be incomplete.
func LoadSchemas(config *configs.Config, state *states.State, components contextComponentFactory) (*Schemas, error) {
schemas := &Schemas{
Providers: map[addrs.Provider]*ProviderSchema{},
Provisioners: map[string]*configschema.Block{},
}
var diags tfdiags.Diagnostics
newDiags := loadProviderSchemas(schemas.Providers, config, state, components)
diags = diags.Append(newDiags)
newDiags = loadProvisionerSchemas(schemas.Provisioners, config, components)
diags = diags.Append(newDiags)
return schemas, diags.Err()
}
func loadProviderSchemas(schemas map[addrs.Provider]*ProviderSchema, config *configs.Config, state *states.State, components contextComponentFactory) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
ensure := func(fqn addrs.Provider) {
name := fqn.String()
if _, exists := schemas[fqn]; exists {
return
}
log.Printf("[TRACE] LoadSchemas: retrieving schema for provider type %q", name)
provider, err := components.ResourceProvider(fqn)
if err != nil {
// We'll put a stub in the map so we won't re-attempt this on
// future calls.
schemas[fqn] = &ProviderSchema{}
diags = diags.Append(
fmt.Errorf("Failed to instantiate provider %q to obtain schema: %s", name, err),
)
return
}
defer func() {
provider.Close()
}()
resp := provider.GetSchema()
if resp.Diagnostics.HasErrors() {
// We'll put a stub in the map so we won't re-attempt this on
// future calls.
schemas[fqn] = &ProviderSchema{}
diags = diags.Append(
fmt.Errorf("Failed to retrieve schema from provider %q: %s", name, resp.Diagnostics.Err()),
)
return
}
s := &ProviderSchema{
Provider: resp.Provider.Block,
ResourceTypes: make(map[string]*configschema.Block),
DataSources: make(map[string]*configschema.Block),
ResourceTypeSchemaVersions: make(map[string]uint64),
}
if resp.Provider.Version < 0 {
// We're not using the version numbers here yet, but we'll check
// for validity anyway in case we start using them in future.
diags = diags.Append(
fmt.Errorf("invalid negative schema version provider configuration for provider %q", name),
)
}
for t, r := range resp.ResourceTypes {
s.ResourceTypes[t] = r.Block
s.ResourceTypeSchemaVersions[t] = uint64(r.Version)
if r.Version < 0 {
diags = diags.Append(
fmt.Errorf("invalid negative schema version for resource type %s in provider %q", t, name),
)
}
}
for t, d := range resp.DataSources {
s.DataSources[t] = d.Block
if d.Version < 0 {
// We're not using the version numbers here yet, but we'll check
// for validity anyway in case we start using them in future.
diags = diags.Append(
fmt.Errorf("invalid negative schema version for data source %s in provider %q", t, name),
)
}
}
schemas[fqn] = s
if resp.ProviderMeta.Block != nil {
s.ProviderMeta = resp.ProviderMeta.Block
}
}
if config != nil {
for _, fqn := range config.ProviderTypes() {
ensure(fqn)
}
}
if state != nil {
needed := providers.AddressedTypesAbs(state.ProviderAddrs())
for _, typeAddr := range needed {
ensure(typeAddr)
}
}
return diags
}
func loadProvisionerSchemas(schemas map[string]*configschema.Block, config *configs.Config, components contextComponentFactory) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
ensure := func(name string) {
if _, exists := schemas[name]; exists {
return
}
log.Printf("[TRACE] LoadSchemas: retrieving schema for provisioner %q", name)
provisioner, err := components.ResourceProvisioner(name)
if err != nil {
// We'll put a stub in the map so we won't re-attempt this on
// future calls.
schemas[name] = &configschema.Block{}
diags = diags.Append(
fmt.Errorf("Failed to instantiate provisioner %q to obtain schema: %s", name, err),
)
return
}
defer func() {
if closer, ok := provisioner.(ResourceProvisionerCloser); ok {
closer.Close()
}
}()
resp := provisioner.GetSchema()
if resp.Diagnostics.HasErrors() {
// We'll put a stub in the map so we won't re-attempt this on
// future calls.
schemas[name] = &configschema.Block{}
diags = diags.Append(
fmt.Errorf("Failed to retrieve schema from provisioner %q: %s", name, resp.Diagnostics.Err()),
)
return
}
schemas[name] = resp.Provisioner
}
if config != nil {
for _, rc := range config.Module.ManagedResources {
for _, pc := range rc.Managed.Provisioners {
ensure(pc.Type)
}
}
// Must also visit our child modules, recursively.
for _, cc := range config.Children {
childDiags := loadProvisionerSchemas(schemas, cc, components)
diags = diags.Append(childDiags)
}
}
return diags
}
// ProviderSchema represents the schema for a provider's own configuration
// and the configuration for some or all of its resources and data sources.
//
// The completeness of this structure depends on how it was constructed.
// When constructed for a configuration, it will generally include only
// resource types and data sources used by that configuration.
type ProviderSchema struct {
Provider *configschema.Block
ProviderMeta *configschema.Block
ResourceTypes map[string]*configschema.Block
DataSources map[string]*configschema.Block
ResourceTypeSchemaVersions map[string]uint64
}
// SchemaForResourceType attempts to find a schema for the given mode and type.
// Returns nil if no such schema is available.
func (ps *ProviderSchema) SchemaForResourceType(mode addrs.ResourceMode, typeName string) (schema *configschema.Block, version uint64) {
switch mode {
case addrs.ManagedResourceMode:
return ps.ResourceTypes[typeName], ps.ResourceTypeSchemaVersions[typeName]
case addrs.DataResourceMode:
// Data resources don't have schema versions right now, since state is discarded for each refresh
return ps.DataSources[typeName], 0
default:
// Shouldn't happen, because the above cases are comprehensive.
return nil, 0
}
}
// SchemaForResourceAddr attempts to find a schema for the mode and type from
// the given resource address. Returns nil if no such schema is available.
func (ps *ProviderSchema) SchemaForResourceAddr(addr addrs.Resource) (schema *configschema.Block, version uint64) {
return ps.SchemaForResourceType(addr.Mode, addr.Type)
}
// ProviderSchemaRequest is used to describe to a ResourceProvider which
// aspects of schema are required, when calling the GetSchema method.
type ProviderSchemaRequest struct {
ResourceTypes []string
DataSources []string
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,267 @@
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

@ -0,0 +1,189 @@
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

@ -0,0 +1,142 @@
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

@ -0,0 +1,145 @@
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

@ -0,0 +1,19 @@
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

@ -0,0 +1,32 @@
package terraform
import "context"
// UIInput is the interface that must be implemented to ask for input
// from this user. This should forward the request to wherever the user
// inputs things to ask for values.
type UIInput interface {
Input(context.Context, *InputOpts) (string, error)
}
// InputOpts are options for asking for input.
type InputOpts struct {
// Id is a unique ID for the question being asked that might be
// used for logging or to look up a prior answered question.
Id string
// Query is a human-friendly question for inputting this value.
Query string
// Description is a description about what this option is. Be wary
// that this will probably be in a terminal so split lines as you see
// necessary.
Description string
// Default will be the value returned if no data is entered.
Default string
// Secret should be true if we are asking for sensitive input.
// If attached to a TTY, Terraform will disable echo.
Secret bool
}

View File

@ -0,0 +1,25 @@
package terraform
import "context"
// MockUIInput is an implementation of UIInput that can be used for tests.
type MockUIInput struct {
InputCalled bool
InputOpts *InputOpts
InputReturnMap map[string]string
InputReturnString string
InputReturnError error
InputFn func(*InputOpts) (string, error)
}
func (i *MockUIInput) Input(ctx context.Context, opts *InputOpts) (string, error) {
i.InputCalled = true
i.InputOpts = opts
if i.InputFn != nil {
return i.InputFn(opts)
}
if i.InputReturnMap != nil {
return i.InputReturnMap[opts.Id], i.InputReturnError
}
return i.InputReturnString, i.InputReturnError
}

View File

@ -0,0 +1,20 @@
package terraform
import (
"context"
"fmt"
)
// PrefixUIInput is an implementation of UIInput that prefixes the ID
// with a string, allowing queries to be namespaced.
type PrefixUIInput struct {
IdPrefix string
QueryPrefix string
UIInput UIInput
}
func (i *PrefixUIInput) Input(ctx context.Context, opts *InputOpts) (string, error) {
opts.Id = fmt.Sprintf("%s.%s", i.IdPrefix, opts.Id)
opts.Query = fmt.Sprintf("%s%s", i.QueryPrefix, opts.Query)
return i.UIInput.Input(ctx, opts)
}

View File

@ -0,0 +1,27 @@
package terraform
import (
"context"
"testing"
)
func TestPrefixUIInput_impl(t *testing.T) {
var _ UIInput = new(PrefixUIInput)
}
func TestPrefixUIInput(t *testing.T) {
input := new(MockUIInput)
prefix := &PrefixUIInput{
IdPrefix: "foo",
UIInput: input,
}
_, err := prefix.Input(context.Background(), &InputOpts{Id: "bar"})
if err != nil {
t.Fatalf("err: %s", err)
}
if input.InputOpts.Id != "foo.bar" {
t.Fatalf("bad: %#v", input.InputOpts)
}
}

View File

@ -0,0 +1,7 @@
package terraform
// UIOutput is the interface that must be implemented to output
// data to the end user.
type UIOutput interface {
Output(string)
}

View File

@ -0,0 +1,9 @@
package terraform
type CallbackUIOutput struct {
OutputFn func(string)
}
func (o *CallbackUIOutput) Output(v string) {
o.OutputFn(v)
}

View File

@ -0,0 +1,9 @@
package terraform
import (
"testing"
)
func TestCallbackUIOutput_impl(t *testing.T) {
var _ UIOutput = new(CallbackUIOutput)
}

View File

@ -0,0 +1,21 @@
package terraform
import "sync"
// MockUIOutput is an implementation of UIOutput that can be used for tests.
type MockUIOutput struct {
sync.Mutex
OutputCalled bool
OutputMessage string
OutputFn func(string)
}
func (o *MockUIOutput) Output(v string) {
o.Lock()
defer o.Unlock()
o.OutputCalled = true
o.OutputMessage = v
if o.OutputFn != nil {
o.OutputFn(v)
}
}

View File

@ -0,0 +1,9 @@
package terraform
import (
"testing"
)
func TestMockUIOutput(t *testing.T) {
var _ UIOutput = new(MockUIOutput)
}

View File

@ -0,0 +1,190 @@
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

@ -0,0 +1,202 @@
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"
}
}
}
}
}
]
}
`

View File

@ -0,0 +1,75 @@
package terraform
import (
"sort"
)
// Semaphore is a wrapper around a channel to provide
// utility methods to clarify that we are treating the
// channel as a semaphore
type Semaphore chan struct{}
// NewSemaphore creates a semaphore that allows up
// to a given limit of simultaneous acquisitions
func NewSemaphore(n int) Semaphore {
if n <= 0 {
panic("semaphore with limit <=0")
}
ch := make(chan struct{}, n)
return Semaphore(ch)
}
// Acquire is used to acquire an available slot.
// Blocks until available.
func (s Semaphore) Acquire() {
s <- struct{}{}
}
// TryAcquire is used to do a non-blocking acquire.
// Returns a bool indicating success
func (s Semaphore) TryAcquire() bool {
select {
case s <- struct{}{}:
return true
default:
return false
}
}
// Release is used to return a slot. Acquire must
// be called as a pre-condition.
func (s Semaphore) Release() {
select {
case <-s:
default:
panic("release without an acquire")
}
}
// strSliceContains checks if a given string is contained in a slice
// When anybody asks why Go needs generics, here you go.
func strSliceContains(haystack []string, needle string) bool {
for _, s := range haystack {
if s == needle {
return true
}
}
return false
}
// deduplicate a slice of strings
func uniqueStrings(s []string) []string {
if len(s) < 2 {
return s
}
sort.Strings(s)
result := make([]string, 1, len(s))
result[0] = s[0]
for i := 1; i < len(s); i++ {
if s[i] != result[len(result)-1] {
result = append(result, s[i])
}
}
return result
}

View File

@ -0,0 +1,91 @@
package terraform
import (
"fmt"
"reflect"
"testing"
"time"
)
func TestSemaphore(t *testing.T) {
s := NewSemaphore(2)
timer := time.AfterFunc(time.Second, func() {
panic("deadlock")
})
defer timer.Stop()
s.Acquire()
if !s.TryAcquire() {
t.Fatalf("should acquire")
}
if s.TryAcquire() {
t.Fatalf("should not acquire")
}
s.Release()
s.Release()
// This release should panic
defer func() {
r := recover()
if r == nil {
t.Fatalf("should panic")
}
}()
s.Release()
}
func TestStrSliceContains(t *testing.T) {
if strSliceContains(nil, "foo") {
t.Fatalf("Bad")
}
if strSliceContains([]string{}, "foo") {
t.Fatalf("Bad")
}
if strSliceContains([]string{"bar"}, "foo") {
t.Fatalf("Bad")
}
if !strSliceContains([]string{"bar", "foo"}, "foo") {
t.Fatalf("Bad")
}
}
func TestUniqueStrings(t *testing.T) {
cases := []struct {
Input []string
Expected []string
}{
{
[]string{},
[]string{},
},
{
[]string{"x"},
[]string{"x"},
},
{
[]string{"a", "b", "c"},
[]string{"a", "b", "c"},
},
{
[]string{"a", "a", "a"},
[]string{"a"},
},
{
[]string{"a", "b", "a", "b", "a", "a"},
[]string{"a", "b"},
},
{
[]string{"c", "b", "a", "c", "b"},
[]string{"a", "b", "c"},
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("unique-%d", i), func(t *testing.T) {
actual := uniqueStrings(tc.Input)
if !reflect.DeepEqual(tc.Expected, actual) {
t.Fatalf("Expected: %q\nGot: %q", tc.Expected, actual)
}
})
}
}

View File

@ -0,0 +1,10 @@
package terraform
import (
"github.com/hashicorp/terraform/version"
)
// Deprecated: Providers should use schema.Provider.TerraformVersion instead
func VersionString() string {
return version.String()
}

View File

@ -0,0 +1,62 @@
package terraform
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/tfdiags"
"github.com/hashicorp/terraform/configs"
tfversion "github.com/hashicorp/terraform/version"
)
// CheckCoreVersionRequirements visits each of the modules in the given
// configuration tree and verifies that any given Core version constraints
// match with the version of Terraform Core that is being used.
//
// The returned diagnostics will contain errors if any constraints do not match.
// The returned diagnostics might also return warnings, which should be
// displayed to the user.
func CheckCoreVersionRequirements(config *configs.Config) tfdiags.Diagnostics {
if config == nil {
return nil
}
var diags tfdiags.Diagnostics
module := config.Module
for _, constraint := range module.CoreVersionConstraints {
if !constraint.Required.Check(tfversion.SemVer) {
switch {
case len(config.Path) == 0:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsupported Terraform Core version",
Detail: fmt.Sprintf(
"This configuration does not support Terraform version %s. To proceed, either choose another supported Terraform version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.",
tfversion.String(),
),
Subject: constraint.DeclRange.Ptr(),
})
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsupported Terraform Core version",
Detail: fmt.Sprintf(
"Module %s (from %s) does not support Terraform version %s. To proceed, either choose another supported Terraform version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.",
config.Path, config.SourceAddr, tfversion.String(),
),
Subject: constraint.DeclRange.Ptr(),
})
}
}
}
for _, c := range config.Children {
childDiags := CheckCoreVersionRequirements(c)
diags = diags.Append(childDiags)
}
return diags
}