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:
parent
c6ab9b1553
commit
a49e7eee8b
|
@ -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
|
@ -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") != ""
|
|
@ -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
|
||||||
|
)
|
|
@ -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]]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
@ -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
|
||||||
|
)
|
|
@ -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]]
|
||||||
|
}
|
|
@ -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
|
||||||
|
`
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
@ -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
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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:"-"`
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
type CallbackUIOutput struct {
|
||||||
|
OutputFn func(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *CallbackUIOutput) Output(v string) {
|
||||||
|
o.OutputFn(v)
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCallbackUIOutput_impl(t *testing.T) {
|
||||||
|
var _ UIOutput = new(CallbackUIOutput)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMockUIOutput(t *testing.T) {
|
||||||
|
var _ UIOutput = new(MockUIOutput)
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue