Merge pull request #12 from hashicorp/f-semantics
Richer Validation, Semantic Checks, Core Refactor
This commit is contained in:
commit
3decae513e
|
@ -4,6 +4,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/helper/config"
|
"github.com/hashicorp/terraform/helper/config"
|
||||||
|
"github.com/hashicorp/terraform/helper/multierror"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/goamz/ec2"
|
"github.com/mitchellh/goamz/ec2"
|
||||||
)
|
)
|
||||||
|
@ -18,6 +19,11 @@ func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []er
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *ResourceProvider) ValidateResource(
|
||||||
|
t string, c *terraform.ResourceConfig) ([]string, []error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error {
|
func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error {
|
||||||
if _, err := config.Decode(&p.Config, c.Config); err != nil {
|
if _, err := config.Decode(&p.Config, c.Config); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -44,7 +50,7 @@ func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return &terraform.MultiError{Errors: errs}
|
return &multierror.Error{Errors: errs}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -109,18 +109,18 @@ func resource_aws_instance_diff(
|
||||||
c *terraform.ResourceConfig,
|
c *terraform.ResourceConfig,
|
||||||
meta interface{}) (*terraform.ResourceDiff, error) {
|
meta interface{}) (*terraform.ResourceDiff, error) {
|
||||||
b := &diff.ResourceBuilder{
|
b := &diff.ResourceBuilder{
|
||||||
CreateComputedAttrs: []string{
|
Attrs: map[string]diff.AttrType{
|
||||||
|
"ami": diff.AttrTypeCreate,
|
||||||
|
"availability_zone": diff.AttrTypeCreate,
|
||||||
|
"instance_type": diff.AttrTypeCreate,
|
||||||
|
},
|
||||||
|
|
||||||
|
ComputedAttrs: []string{
|
||||||
"public_dns",
|
"public_dns",
|
||||||
"public_ip",
|
"public_ip",
|
||||||
"private_dns",
|
"private_dns",
|
||||||
"private_ip",
|
"private_ip",
|
||||||
},
|
},
|
||||||
|
|
||||||
RequiresNewAttrs: []string{
|
|
||||||
"ami",
|
|
||||||
"availability_zone",
|
|
||||||
"instance_type",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.Diff(s, c)
|
return b.Diff(s, c)
|
||||||
|
|
|
@ -13,9 +13,9 @@ import (
|
||||||
// ApplyCommand is a Command implementation that applies a Terraform
|
// ApplyCommand is a Command implementation that applies a Terraform
|
||||||
// configuration and actually builds or changes infrastructure.
|
// configuration and actually builds or changes infrastructure.
|
||||||
type ApplyCommand struct {
|
type ApplyCommand struct {
|
||||||
ShutdownCh <-chan struct{}
|
ShutdownCh <-chan struct{}
|
||||||
TFConfig *terraform.Config
|
ContextOpts *terraform.ContextOpts
|
||||||
Ui cli.Ui
|
Ui cli.Ui
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ApplyCommand) Run(args []string) int {
|
func (c *ApplyCommand) Run(args []string) int {
|
||||||
|
@ -44,30 +44,26 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
stateOutPath = statePath
|
stateOutPath = statePath
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Terraform right away
|
|
||||||
c.TFConfig.Hooks = append(c.TFConfig.Hooks, &UiHook{Ui: c.Ui})
|
|
||||||
tf, err := terraform.New(c.TFConfig)
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error initializing Terraform: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to read a plan from the path given. This is how we test that
|
|
||||||
// it is a plan or not (kind of jank, but if it quacks like a duck...)
|
|
||||||
planStatePath := statePath
|
planStatePath := statePath
|
||||||
if init {
|
if init {
|
||||||
planStatePath = ""
|
planStatePath = ""
|
||||||
}
|
}
|
||||||
plan, err := PlanArg(configPath, planStatePath, tf)
|
|
||||||
|
// Initialize Terraform right away
|
||||||
|
c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui})
|
||||||
|
ctx, err := ContextArg(configPath, planStatePath, c.ContextOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(err.Error())
|
c.Ui.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
if !validateContext(ctx, c.Ui) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
errCh := make(chan error)
|
errCh := make(chan error)
|
||||||
stateCh := make(chan *terraform.State)
|
stateCh := make(chan *terraform.State)
|
||||||
go func() {
|
go func() {
|
||||||
state, err := tf.Apply(plan)
|
state, err := ctx.Apply()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- err
|
errCh <- err
|
||||||
return
|
return
|
||||||
|
@ -83,7 +79,7 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
c.Ui.Output("Interrupt received. Gracefully shutting down...")
|
c.Ui.Output("Interrupt received. Gracefully shutting down...")
|
||||||
|
|
||||||
// Stop execution
|
// Stop execution
|
||||||
tf.Stop()
|
ctx.Stop()
|
||||||
|
|
||||||
// Still get the result, since there is still one
|
// Still get the result, since there is still one
|
||||||
select {
|
select {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/config"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
@ -16,8 +17,8 @@ func TestApply(t *testing.T) {
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &ApplyCommand{
|
c := &ApplyCommand{
|
||||||
TFConfig: testTFConfig(p),
|
ContextOpts: testCtxConfig(p),
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
|
@ -48,15 +49,35 @@ func TestApply(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApply_configInvalid(t *testing.T) {
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &ApplyCommand{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-init",
|
||||||
|
testTempFile(t),
|
||||||
|
testFixturePath("apply-config-invalid"),
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 1 {
|
||||||
|
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestApply_plan(t *testing.T) {
|
func TestApply_plan(t *testing.T) {
|
||||||
planPath := testPlanFile(t, new(terraform.Plan))
|
planPath := testPlanFile(t, &terraform.Plan{
|
||||||
|
Config: new(config.Config),
|
||||||
|
})
|
||||||
statePath := testTempFile(t)
|
statePath := testTempFile(t)
|
||||||
|
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &ApplyCommand{
|
c := &ApplyCommand{
|
||||||
TFConfig: testTFConfig(p),
|
ContextOpts: testCtxConfig(p),
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
|
@ -97,9 +118,9 @@ func TestApply_shutdown(t *testing.T) {
|
||||||
shutdownCh := make(chan struct{})
|
shutdownCh := make(chan struct{})
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &ApplyCommand{
|
c := &ApplyCommand{
|
||||||
ShutdownCh: shutdownCh,
|
ContextOpts: testCtxConfig(p),
|
||||||
TFConfig: testTFConfig(p),
|
ShutdownCh: shutdownCh,
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
p.DiffFn = func(
|
p.DiffFn = func(
|
||||||
|
@ -197,8 +218,8 @@ func TestApply_state(t *testing.T) {
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &ApplyCommand{
|
c := &ApplyCommand{
|
||||||
TFConfig: testTFConfig(p),
|
ContextOpts: testCtxConfig(p),
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the apply command pointing to our existing state
|
// Run the apply command pointing to our existing state
|
||||||
|
@ -244,8 +265,8 @@ func TestApply_stateNoExist(t *testing.T) {
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &ApplyCommand{
|
c := &ApplyCommand{
|
||||||
TFConfig: testTFConfig(p),
|
ContextOpts: testCtxConfig(p),
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
|
|
|
@ -6,19 +6,20 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PlanArg(
|
func ContextArg(
|
||||||
path string,
|
path string,
|
||||||
statePath string,
|
statePath string,
|
||||||
tf *terraform.Terraform) (*terraform.Plan, error) {
|
opts *terraform.ContextOpts) (*terraform.Context, error) {
|
||||||
// First try to just read the plan directly from the path given.
|
// First try to just read the plan directly from the path given.
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
plan, err := terraform.ReadPlan(f)
|
plan, err := terraform.ReadPlan(f)
|
||||||
f.Close()
|
f.Close()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return plan, nil
|
return plan.Context(opts), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,14 +56,47 @@ func PlanArg(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error loading config: %s", err)
|
return nil, fmt.Errorf("Error loading config: %s", err)
|
||||||
}
|
}
|
||||||
|
if err := config.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("Error validating config: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
plan, err := tf.Plan(&terraform.PlanOpts{
|
opts.Config = config
|
||||||
Config: config,
|
opts.State = state
|
||||||
State: state,
|
ctx := terraform.NewContext(opts)
|
||||||
})
|
|
||||||
if err != nil {
|
if _, err := ctx.Plan(nil); err != nil {
|
||||||
return nil, fmt.Errorf("Error running plan: %s", err)
|
return nil, fmt.Errorf("Error running plan: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return plan, nil
|
return ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateContext(ctx *terraform.Context, ui cli.Ui) bool {
|
||||||
|
if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 {
|
||||||
|
ui.Output(
|
||||||
|
"There are warnings and/or errors related to your configuration. Please\n" +
|
||||||
|
"fix these before continuing.\n")
|
||||||
|
|
||||||
|
if len(ws) > 0 {
|
||||||
|
ui.Output("Warnings:\n")
|
||||||
|
for _, w := range ws {
|
||||||
|
ui.Output(fmt.Sprintf(" * %s", w))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(es) > 0 {
|
||||||
|
ui.Output("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(es) > 0 {
|
||||||
|
ui.Output("Errors:\n")
|
||||||
|
for _, e := range es {
|
||||||
|
ui.Output(fmt.Sprintf(" * %s", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,8 @@ func testFixturePath(name string) string {
|
||||||
return filepath.Join(fixtureDir, name, "main.tf")
|
return filepath.Join(fixtureDir, name, "main.tf")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTFConfig(p terraform.ResourceProvider) *terraform.Config {
|
func testCtxConfig(p terraform.ResourceProvider) *terraform.ContextOpts {
|
||||||
return &terraform.Config{
|
return &terraform.ContextOpts{
|
||||||
Providers: map[string]terraform.ResourceProviderFactory{
|
Providers: map[string]terraform.ResourceProviderFactory{
|
||||||
"test": func() (terraform.ResourceProvider, error) {
|
"test": func() (terraform.ResourceProvider, error) {
|
||||||
return p, nil
|
return p, nil
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is a structure used to configure many commands with Terraform
|
||||||
|
// configurations.
|
||||||
|
type Config struct {
|
||||||
|
Hooks []terraform.Hook
|
||||||
|
Providers map[string]terraform.ResourceProviderFactory
|
||||||
|
Ui cli.Ui
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ContextOpts() *terraform.ContextOpts {
|
||||||
|
hooks := make([]terraform.Hook, len(c.Hooks)+1)
|
||||||
|
copy(hooks, c.Hooks)
|
||||||
|
hooks[len(c.Hooks)] = &UiHook{Ui: c.Ui}
|
||||||
|
|
||||||
|
return &terraform.ContextOpts{
|
||||||
|
Hooks: hooks,
|
||||||
|
Providers: c.Providers,
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,8 +15,8 @@ import (
|
||||||
// GraphCommand is a Command implementation that takes a Terraform
|
// GraphCommand is a Command implementation that takes a Terraform
|
||||||
// configuration and outputs the dependency tree in graphical form.
|
// configuration and outputs the dependency tree in graphical form.
|
||||||
type GraphCommand struct {
|
type GraphCommand struct {
|
||||||
TFConfig *terraform.Config
|
ContextOpts *terraform.ContextOpts
|
||||||
Ui cli.Ui
|
Ui cli.Ui
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GraphCommand) Run(args []string) int {
|
func (c *GraphCommand) Run(args []string) int {
|
||||||
|
@ -41,7 +41,7 @@ func (c *GraphCommand) Run(args []string) int {
|
||||||
|
|
||||||
g, err := terraform.Graph(&terraform.GraphOpts{
|
g, err := terraform.Graph(&terraform.GraphOpts{
|
||||||
Config: conf,
|
Config: conf,
|
||||||
Providers: c.TFConfig.Providers,
|
Providers: c.ContextOpts.Providers,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error creating graph: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error creating graph: %s", err))
|
||||||
|
|
|
@ -15,8 +15,8 @@ import (
|
||||||
// PlanCommand is a Command implementation that compares a Terraform
|
// PlanCommand is a Command implementation that compares a Terraform
|
||||||
// configuration to an actual infrastructure and shows the differences.
|
// configuration to an actual infrastructure and shows the differences.
|
||||||
type PlanCommand struct {
|
type PlanCommand struct {
|
||||||
TFConfig *terraform.Config
|
ContextOpts *terraform.ContextOpts
|
||||||
Ui cli.Ui
|
Ui cli.Ui
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PlanCommand) Run(args []string) int {
|
func (c *PlanCommand) Run(args []string) int {
|
||||||
|
@ -65,26 +65,22 @@ func (c *PlanCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
c.TFConfig.Hooks = append(c.TFConfig.Hooks, &UiHook{Ui: c.Ui})
|
c.ContextOpts.Config = b
|
||||||
tf, err := terraform.New(c.TFConfig)
|
c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui})
|
||||||
if err != nil {
|
c.ContextOpts.State = state
|
||||||
c.Ui.Error(fmt.Sprintf("Error initializing Terraform: %s", err))
|
ctx := terraform.NewContext(c.ContextOpts)
|
||||||
|
if !validateContext(ctx, c.Ui) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if refresh {
|
if refresh {
|
||||||
state, err = tf.Refresh(b, state)
|
if _, err := ctx.Refresh(); err != nil {
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plan, err := tf.Plan(&terraform.PlanOpts{
|
plan, err := ctx.Plan(&terraform.PlanOpts{Destroy: destroy})
|
||||||
Config: b,
|
|
||||||
Destroy: destroy,
|
|
||||||
State: state,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error running plan: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error running plan: %s", err))
|
||||||
return 1
|
return 1
|
||||||
|
|
|
@ -26,8 +26,8 @@ func TestPlan_destroy(t *testing.T) {
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &PlanCommand{
|
c := &PlanCommand{
|
||||||
TFConfig: testTFConfig(p),
|
ContextOpts: testCtxConfig(p),
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
|
@ -51,8 +51,8 @@ func TestPlan_noState(t *testing.T) {
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &PlanCommand{
|
c := &PlanCommand{
|
||||||
TFConfig: testTFConfig(p),
|
ContextOpts: testCtxConfig(p),
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
|
@ -87,8 +87,8 @@ func TestPlan_outPath(t *testing.T) {
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &PlanCommand{
|
c := &PlanCommand{
|
||||||
TFConfig: testTFConfig(p),
|
ContextOpts: testCtxConfig(p),
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
p.DiffReturn = &terraform.ResourceDiff{
|
p.DiffReturn = &terraform.ResourceDiff{
|
||||||
|
@ -118,8 +118,8 @@ func TestPlan_refresh(t *testing.T) {
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &PlanCommand{
|
c := &PlanCommand{
|
||||||
TFConfig: testTFConfig(p),
|
ContextOpts: testCtxConfig(p),
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
|
@ -162,8 +162,8 @@ func TestPlan_state(t *testing.T) {
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &PlanCommand{
|
c := &PlanCommand{
|
||||||
TFConfig: testTFConfig(p),
|
ContextOpts: testCtxConfig(p),
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
|
|
|
@ -15,8 +15,8 @@ import (
|
||||||
// RefreshCommand is a cli.Command implementation that refreshes the state
|
// RefreshCommand is a cli.Command implementation that refreshes the state
|
||||||
// file.
|
// file.
|
||||||
type RefreshCommand struct {
|
type RefreshCommand struct {
|
||||||
TFConfig *terraform.Config
|
ContextOpts *terraform.ContextOpts
|
||||||
Ui cli.Ui
|
Ui cli.Ui
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RefreshCommand) Run(args []string) int {
|
func (c *RefreshCommand) Run(args []string) int {
|
||||||
|
@ -66,14 +66,14 @@ func (c *RefreshCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
c.TFConfig.Hooks = append(c.TFConfig.Hooks, &UiHook{Ui: c.Ui})
|
c.ContextOpts.Config = b
|
||||||
tf, err := terraform.New(c.TFConfig)
|
c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui})
|
||||||
if err != nil {
|
ctx := terraform.NewContext(c.ContextOpts)
|
||||||
c.Ui.Error(fmt.Sprintf("Error initializing Terraform: %s", err))
|
if !validateContext(ctx, c.Ui) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
state, err = tf.Refresh(b, state)
|
state, err = ctx.Refresh()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
|
|
|
@ -30,8 +30,8 @@ func TestRefresh(t *testing.T) {
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &RefreshCommand{
|
c := &RefreshCommand{
|
||||||
TFConfig: testTFConfig(p),
|
ContextOpts: testCtxConfig(p),
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
p.RefreshFn = nil
|
p.RefreshFn = nil
|
||||||
|
@ -96,8 +96,8 @@ func TestRefresh_outPath(t *testing.T) {
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &RefreshCommand{
|
c := &RefreshCommand{
|
||||||
TFConfig: testTFConfig(p),
|
ContextOpts: testCtxConfig(p),
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
p.RefreshFn = nil
|
p.RefreshFn = nil
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
resource "test_instance" "foo" {
|
||||||
|
ami = "${var.nope}"
|
||||||
|
}
|
18
commands.go
18
commands.go
|
@ -29,30 +29,30 @@ func init() {
|
||||||
Commands = map[string]cli.CommandFactory{
|
Commands = map[string]cli.CommandFactory{
|
||||||
"apply": func() (cli.Command, error) {
|
"apply": func() (cli.Command, error) {
|
||||||
return &command.ApplyCommand{
|
return &command.ApplyCommand{
|
||||||
ShutdownCh: makeShutdownCh(),
|
ShutdownCh: makeShutdownCh(),
|
||||||
TFConfig: &TFConfig,
|
ContextOpts: &ContextOpts,
|
||||||
Ui: Ui,
|
Ui: Ui,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
"graph": func() (cli.Command, error) {
|
"graph": func() (cli.Command, error) {
|
||||||
return &command.GraphCommand{
|
return &command.GraphCommand{
|
||||||
TFConfig: &TFConfig,
|
ContextOpts: &ContextOpts,
|
||||||
Ui: Ui,
|
Ui: Ui,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
"plan": func() (cli.Command, error) {
|
"plan": func() (cli.Command, error) {
|
||||||
return &command.PlanCommand{
|
return &command.PlanCommand{
|
||||||
TFConfig: &TFConfig,
|
ContextOpts: &ContextOpts,
|
||||||
Ui: Ui,
|
Ui: Ui,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
"refresh": func() (cli.Command, error) {
|
"refresh": func() (cli.Command, error) {
|
||||||
return &command.RefreshCommand{
|
return &command.RefreshCommand{
|
||||||
TFConfig: &TFConfig,
|
ContextOpts: &ContextOpts,
|
||||||
Ui: Ui,
|
Ui: Ui,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -11,12 +11,6 @@ import (
|
||||||
"github.com/mitchellh/osext"
|
"github.com/mitchellh/osext"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TFConfig is the global base configuration that has the
|
|
||||||
// basic providers registered. Users of this configuration
|
|
||||||
// should copy it (call the Copy method) before using it so
|
|
||||||
// that it isn't corrupted.
|
|
||||||
var TFConfig terraform.Config
|
|
||||||
|
|
||||||
// Config is the structure of the configuration for the Terraform CLI.
|
// Config is the structure of the configuration for the Terraform CLI.
|
||||||
//
|
//
|
||||||
// This is not the configuration for Terraform itself. That is in the
|
// This is not the configuration for Terraform itself. That is in the
|
||||||
|
@ -29,6 +23,9 @@ type Config struct {
|
||||||
// can be overridden by user configurations.
|
// can be overridden by user configurations.
|
||||||
var BuiltinConfig Config
|
var BuiltinConfig Config
|
||||||
|
|
||||||
|
// ContextOpts are the global ContextOpts we use to initialize the CLI.
|
||||||
|
var ContextOpts terraform.ContextOpts
|
||||||
|
|
||||||
// Put the parse flags we use for libucl in a constant so we can get
|
// Put the parse flags we use for libucl in a constant so we can get
|
||||||
// equally behaving parsing everywhere.
|
// equally behaving parsing everywhere.
|
||||||
const libuclParseFlags = libucl.ParserKeyLowercase
|
const libuclParseFlags = libucl.ParserKeyLowercase
|
||||||
|
|
|
@ -5,6 +5,8 @@ package config
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/multierror"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is the configuration that comes from loading a collection
|
// Config is the configuration that comes from loading a collection
|
||||||
|
@ -89,12 +91,76 @@ func (r *Resource) Id() string {
|
||||||
|
|
||||||
// Validate does some basic semantic checking of the configuration.
|
// Validate does some basic semantic checking of the configuration.
|
||||||
func (c *Config) Validate() error {
|
func (c *Config) Validate() error {
|
||||||
// TODO(mitchellh): make sure all referenced variables exist
|
var errs []error
|
||||||
// TODO(mitchellh): make sure types/names have valid values (characters)
|
|
||||||
|
vars := c.allVariables()
|
||||||
|
|
||||||
|
// Check for references to user variables that do not actually
|
||||||
|
// exist and record those errors.
|
||||||
|
for source, v := range vars {
|
||||||
|
uv, ok := v.(*UserVariable)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := c.Variables[uv.Name]; !ok {
|
||||||
|
errs = append(errs, fmt.Errorf(
|
||||||
|
"%s: unknown variable referenced: %s",
|
||||||
|
source,
|
||||||
|
uv.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that all references to resources are valid
|
||||||
|
resources := make(map[string]struct{})
|
||||||
|
for _, r := range c.Resources {
|
||||||
|
resources[r.Id()] = struct{}{}
|
||||||
|
}
|
||||||
|
for source, v := range vars {
|
||||||
|
rv, ok := v.(*ResourceVariable)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
id := fmt.Sprintf("%s.%s", rv.Type, rv.Name)
|
||||||
|
if _, ok := resources[id]; !ok {
|
||||||
|
errs = append(errs, fmt.Errorf(
|
||||||
|
"%s: unknown resource '%s' referenced in variable %s",
|
||||||
|
source,
|
||||||
|
id,
|
||||||
|
rv.FullKey()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return &multierror.Error{Errors: errs}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allVariables is a helper that returns a mapping of all the interpolated
|
||||||
|
// variables within the configuration. This is used to verify references
|
||||||
|
// are valid in the Validate step.
|
||||||
|
func (c *Config) allVariables() map[string]InterpolatedVariable {
|
||||||
|
result := make(map[string]InterpolatedVariable)
|
||||||
|
for n, pc := range c.ProviderConfigs {
|
||||||
|
source := fmt.Sprintf("provider config '%s'", n)
|
||||||
|
for _, v := range pc.RawConfig.Variables {
|
||||||
|
result[source] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rc := range c.Resources {
|
||||||
|
source := fmt.Sprintf("resource '%s'", rc.Id())
|
||||||
|
for _, v := range rc.RawConfig.Variables {
|
||||||
|
result[source] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// Required tests whether a variable is required or not.
|
// Required tests whether a variable is required or not.
|
||||||
func (v *Variable) Required() bool {
|
func (v *Variable) Required() bool {
|
||||||
return !v.defaultSet
|
return !v.defaultSet
|
||||||
|
|
|
@ -1,12 +1,34 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This is the directory where our test fixtures are.
|
// This is the directory where our test fixtures are.
|
||||||
const fixtureDir = "./test-fixtures"
|
const fixtureDir = "./test-fixtures"
|
||||||
|
|
||||||
|
func TestConfigValidate(t *testing.T) {
|
||||||
|
c := testConfig(t, "validate-good")
|
||||||
|
if err := c.Validate(); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigValidate_unknownResourceVar(t *testing.T) {
|
||||||
|
c := testConfig(t, "validate-unknown-resource-var")
|
||||||
|
if err := c.Validate(); err == nil {
|
||||||
|
t.Fatal("should not be valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigValidate_unknownVar(t *testing.T) {
|
||||||
|
c := testConfig(t, "validate-unknownvar")
|
||||||
|
if err := c.Validate(); err == nil {
|
||||||
|
t.Fatal("should not be valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewResourceVariable(t *testing.T) {
|
func TestNewResourceVariable(t *testing.T) {
|
||||||
v, err := NewResourceVariable("foo.bar.baz")
|
v, err := NewResourceVariable("foo.bar.baz")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -55,3 +77,12 @@ func TestProviderConfigName(t *testing.T) {
|
||||||
t.Fatalf("bad: %s", n)
|
t.Fatalf("bad: %s", n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testConfig(t *testing.T, name string) *Config {
|
||||||
|
c, err := Load(filepath.Join(fixtureDir, name, "main.tf"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
variable "foo" {
|
||||||
|
default = "bar";
|
||||||
|
description = "bar";
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "aws" {
|
||||||
|
access_key = "foo";
|
||||||
|
secret_key = "bar";
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "do" {
|
||||||
|
api_key = "${var.foo}";
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_security_group" "firewall" {
|
||||||
|
}
|
||||||
|
|
||||||
|
resource aws_instance "web" {
|
||||||
|
ami = "${var.foo}"
|
||||||
|
security_groups = [
|
||||||
|
"foo",
|
||||||
|
"${aws_security_group.firewall.foo}"
|
||||||
|
]
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
device_index = 0
|
||||||
|
description = "Main network interface"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
resource "aws_instance" "web" {
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "db" {
|
||||||
|
ami = "${aws_instance.loadbalancer.foo}"
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
variable "foo" {
|
||||||
|
default = "bar";
|
||||||
|
description = "bar";
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "do" {
|
||||||
|
api_key = "${var.bar}";
|
||||||
|
}
|
|
@ -1,18 +1,18 @@
|
||||||
package terraform
|
package multierror
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MultiError is an error type to track multiple errors. This is used to
|
// Error is an error type to track multiple errors. This is used to
|
||||||
// accumulate errors in cases such as configuration parsing, and returning
|
// accumulate errors in cases such as configuration parsing, and returning
|
||||||
// them as a single error.
|
// them as a single error.
|
||||||
type MultiError struct {
|
type Error struct {
|
||||||
Errors []error
|
Errors []error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *MultiError) Error() string {
|
func (e *Error) Error() string {
|
||||||
points := make([]string, len(e.Errors))
|
points := make([]string, len(e.Errors))
|
||||||
for i, err := range e.Errors {
|
for i, err := range e.Errors {
|
||||||
points[i] = fmt.Sprintf("* %s", err)
|
points[i] = fmt.Sprintf("* %s", err)
|
||||||
|
@ -23,18 +23,18 @@ func (e *MultiError) Error() string {
|
||||||
len(e.Errors), strings.Join(points, "\n"))
|
len(e.Errors), strings.Join(points, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MultiErrorAppend is a helper function that will append more errors
|
// ErrorAppend is a helper function that will append more errors
|
||||||
// onto a MultiError in order to create a larger multi-error. If the
|
// onto a Error in order to create a larger multi-error. If the
|
||||||
// original error is not a MultiError, it will be turned into one.
|
// original error is not a Error, it will be turned into one.
|
||||||
func MultiErrorAppend(err error, errs ...error) *MultiError {
|
func ErrorAppend(err error, errs ...error) *Error {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = new(MultiError)
|
err = new(Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch err := err.(type) {
|
switch err := err.(type) {
|
||||||
case *MultiError:
|
case *Error:
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = new(MultiError)
|
err = new(Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
err.Errors = append(err.Errors, errs...)
|
err.Errors = append(err.Errors, errs...)
|
||||||
|
@ -43,7 +43,7 @@ func MultiErrorAppend(err error, errs ...error) *MultiError {
|
||||||
newErrs := make([]error, len(errs)+1)
|
newErrs := make([]error, len(errs)+1)
|
||||||
newErrs[0] = err
|
newErrs[0] = err
|
||||||
copy(newErrs[1:], errs)
|
copy(newErrs[1:], errs)
|
||||||
return &MultiError{
|
return &Error{
|
||||||
Errors: newErrs,
|
Errors: newErrs,
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,19 +1,19 @@
|
||||||
package terraform
|
package multierror
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMultiError_Impl(t *testing.T) {
|
func TestError_Impl(t *testing.T) {
|
||||||
var raw interface{}
|
var raw interface{}
|
||||||
raw = &MultiError{}
|
raw = &Error{}
|
||||||
if _, ok := raw.(error); !ok {
|
if _, ok := raw.(error); !ok {
|
||||||
t.Fatal("MultiError must implement error")
|
t.Fatal("Error must implement error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultiErrorError(t *testing.T) {
|
func TestErrorError(t *testing.T) {
|
||||||
expected := `2 error(s) occurred:
|
expected := `2 error(s) occurred:
|
||||||
|
|
||||||
* foo
|
* foo
|
||||||
|
@ -24,32 +24,32 @@ func TestMultiErrorError(t *testing.T) {
|
||||||
errors.New("bar"),
|
errors.New("bar"),
|
||||||
}
|
}
|
||||||
|
|
||||||
multi := &MultiError{errors}
|
multi := &Error{errors}
|
||||||
if multi.Error() != expected {
|
if multi.Error() != expected {
|
||||||
t.Fatalf("bad: %s", multi.Error())
|
t.Fatalf("bad: %s", multi.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultiErrorAppend_MultiError(t *testing.T) {
|
func TestErrorAppend_Error(t *testing.T) {
|
||||||
original := &MultiError{
|
original := &Error{
|
||||||
Errors: []error{errors.New("foo")},
|
Errors: []error{errors.New("foo")},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := MultiErrorAppend(original, errors.New("bar"))
|
result := ErrorAppend(original, errors.New("bar"))
|
||||||
if len(result.Errors) != 2 {
|
if len(result.Errors) != 2 {
|
||||||
t.Fatalf("wrong len: %d", len(result.Errors))
|
t.Fatalf("wrong len: %d", len(result.Errors))
|
||||||
}
|
}
|
||||||
|
|
||||||
original = &MultiError{}
|
original = &Error{}
|
||||||
result = MultiErrorAppend(original, errors.New("bar"))
|
result = ErrorAppend(original, errors.New("bar"))
|
||||||
if len(result.Errors) != 1 {
|
if len(result.Errors) != 1 {
|
||||||
t.Fatalf("wrong len: %d", len(result.Errors))
|
t.Fatalf("wrong len: %d", len(result.Errors))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultiErrorAppend_NonMultiError(t *testing.T) {
|
func TestErrorAppend_NonError(t *testing.T) {
|
||||||
original := errors.New("foo")
|
original := errors.New("foo")
|
||||||
result := MultiErrorAppend(original, errors.New("bar"))
|
result := ErrorAppend(original, errors.New("bar"))
|
||||||
if len(result.Errors) != 2 {
|
if len(result.Errors) != 2 {
|
||||||
t.Fatalf("wrong len: %d", len(result.Errors))
|
t.Fatalf("wrong len: %d", len(result.Errors))
|
||||||
}
|
}
|
2
main.go
2
main.go
|
@ -84,7 +84,7 @@ func wrappedMain() int {
|
||||||
defer plugin.CleanupClients()
|
defer plugin.CleanupClients()
|
||||||
|
|
||||||
// Initialize the TFConfig settings for the commands...
|
// Initialize the TFConfig settings for the commands...
|
||||||
TFConfig.Providers = config.ProviderFactories()
|
ContextOpts.Providers = config.ProviderFactories()
|
||||||
|
|
||||||
// Get the command line args. We shortcut "--version" and "-v" to
|
// Get the command line args. We shortcut "--version" and "-v" to
|
||||||
// just show the version.
|
// just show the version.
|
||||||
|
|
|
@ -35,6 +35,30 @@ func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []er
|
||||||
return resp.Warnings, errs
|
return resp.Warnings, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *ResourceProvider) ValidateResource(
|
||||||
|
t string, c *terraform.ResourceConfig) ([]string, []error) {
|
||||||
|
var resp ResourceProviderValidateResourceResponse
|
||||||
|
args := ResourceProviderValidateResourceArgs{
|
||||||
|
Config: c,
|
||||||
|
Type: t,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.Client.Call(p.Name+".ValidateResource", &args, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, []error{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
if len(resp.Errors) > 0 {
|
||||||
|
errs = make([]error, len(resp.Errors))
|
||||||
|
for i, err := range resp.Errors {
|
||||||
|
errs[i] = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Warnings, errs
|
||||||
|
}
|
||||||
|
|
||||||
func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error {
|
func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error {
|
||||||
var resp ResourceProviderConfigureResponse
|
var resp ResourceProviderConfigureResponse
|
||||||
err := p.Client.Call(p.Name+".Configure", c, &resp)
|
err := p.Client.Call(p.Name+".Configure", c, &resp)
|
||||||
|
@ -157,6 +181,16 @@ type ResourceProviderValidateResponse struct {
|
||||||
Errors []*BasicError
|
Errors []*BasicError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResourceProviderValidateResourceArgs struct {
|
||||||
|
Config *terraform.ResourceConfig
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceProviderValidateResourceResponse struct {
|
||||||
|
Warnings []string
|
||||||
|
Errors []*BasicError
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ResourceProviderServer) Validate(
|
func (s *ResourceProviderServer) Validate(
|
||||||
args *ResourceProviderValidateArgs,
|
args *ResourceProviderValidateArgs,
|
||||||
reply *ResourceProviderValidateResponse) error {
|
reply *ResourceProviderValidateResponse) error {
|
||||||
|
@ -172,6 +206,21 @@ func (s *ResourceProviderServer) Validate(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ResourceProviderServer) ValidateResource(
|
||||||
|
args *ResourceProviderValidateResourceArgs,
|
||||||
|
reply *ResourceProviderValidateResourceResponse) error {
|
||||||
|
warns, errs := s.Provider.ValidateResource(args.Type, args.Config)
|
||||||
|
berrs := make([]*BasicError, len(errs))
|
||||||
|
for i, err := range errs {
|
||||||
|
berrs[i] = NewBasicError(err)
|
||||||
|
}
|
||||||
|
*reply = ResourceProviderValidateResourceResponse{
|
||||||
|
Warnings: warns,
|
||||||
|
Errors: berrs,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ResourceProviderServer) Configure(
|
func (s *ResourceProviderServer) Configure(
|
||||||
config *terraform.ResourceConfig,
|
config *terraform.ResourceConfig,
|
||||||
reply *ResourceProviderConfigureResponse) error {
|
reply *ResourceProviderConfigureResponse) error {
|
||||||
|
|
|
@ -341,3 +341,106 @@ func TestResourceProvider_validate_warns(t *testing.T) {
|
||||||
t.Fatalf("bad: %#v", w)
|
t.Fatalf("bad: %#v", w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResourceProvider_validateResource(t *testing.T) {
|
||||||
|
p := new(terraform.MockResourceProvider)
|
||||||
|
client, server := testClientServer(t)
|
||||||
|
name, err := Register(server, p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
provider := &ResourceProvider{Client: client, Name: name}
|
||||||
|
|
||||||
|
// Configure
|
||||||
|
config := &terraform.ResourceConfig{
|
||||||
|
Raw: map[string]interface{}{"foo": "bar"},
|
||||||
|
}
|
||||||
|
w, e := provider.ValidateResource("foo", config)
|
||||||
|
if !p.ValidateResourceCalled {
|
||||||
|
t.Fatal("configure should be called")
|
||||||
|
}
|
||||||
|
if p.ValidateResourceType != "foo" {
|
||||||
|
t.Fatalf("bad: %#v", p.ValidateResourceType)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
|
||||||
|
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
|
||||||
|
}
|
||||||
|
if w != nil {
|
||||||
|
t.Fatalf("bad: %#v", w)
|
||||||
|
}
|
||||||
|
if e != nil {
|
||||||
|
t.Fatalf("bad: %#v", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceProvider_validateResource_errors(t *testing.T) {
|
||||||
|
p := new(terraform.MockResourceProvider)
|
||||||
|
p.ValidateResourceReturnErrors = []error{errors.New("foo")}
|
||||||
|
|
||||||
|
client, server := testClientServer(t)
|
||||||
|
name, err := Register(server, p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
provider := &ResourceProvider{Client: client, Name: name}
|
||||||
|
|
||||||
|
// Configure
|
||||||
|
config := &terraform.ResourceConfig{
|
||||||
|
Raw: map[string]interface{}{"foo": "bar"},
|
||||||
|
}
|
||||||
|
w, e := provider.ValidateResource("foo", config)
|
||||||
|
if !p.ValidateResourceCalled {
|
||||||
|
t.Fatal("configure should be called")
|
||||||
|
}
|
||||||
|
if p.ValidateResourceType != "foo" {
|
||||||
|
t.Fatalf("bad: %#v", p.ValidateResourceType)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
|
||||||
|
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
|
||||||
|
}
|
||||||
|
if w != nil {
|
||||||
|
t.Fatalf("bad: %#v", w)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e) != 1 {
|
||||||
|
t.Fatalf("bad: %#v", e)
|
||||||
|
}
|
||||||
|
if e[0].Error() != "foo" {
|
||||||
|
t.Fatalf("bad: %#v", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceProvider_validateResource_warns(t *testing.T) {
|
||||||
|
p := new(terraform.MockResourceProvider)
|
||||||
|
p.ValidateResourceReturnWarns = []string{"foo"}
|
||||||
|
|
||||||
|
client, server := testClientServer(t)
|
||||||
|
name, err := Register(server, p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
provider := &ResourceProvider{Client: client, Name: name}
|
||||||
|
|
||||||
|
// Configure
|
||||||
|
config := &terraform.ResourceConfig{
|
||||||
|
Raw: map[string]interface{}{"foo": "bar"},
|
||||||
|
}
|
||||||
|
w, e := provider.ValidateResource("foo", config)
|
||||||
|
if !p.ValidateResourceCalled {
|
||||||
|
t.Fatal("configure should be called")
|
||||||
|
}
|
||||||
|
if p.ValidateResourceType != "foo" {
|
||||||
|
t.Fatalf("bad: %#v", p.ValidateResourceType)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
|
||||||
|
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
|
||||||
|
}
|
||||||
|
if e != nil {
|
||||||
|
t.Fatalf("bad: %#v", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{"foo"}
|
||||||
|
if !reflect.DeepEqual(w, expected) {
|
||||||
|
t.Fatalf("bad: %#v", w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,605 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/config"
|
||||||
|
"github.com/hashicorp/terraform/depgraph"
|
||||||
|
"github.com/hashicorp/terraform/helper/multierror"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a function type used to implement a walker for the resource
|
||||||
|
// tree internally on the Terraform structure.
|
||||||
|
type genericWalkFunc func(*Resource) (map[string]string, error)
|
||||||
|
|
||||||
|
// Context represents all the context that Terraform needs in order to
|
||||||
|
// perform operations on infrastructure. This structure is built using
|
||||||
|
// ContextOpts and NewContext. See the documentation for those.
|
||||||
|
//
|
||||||
|
// Additionally, a context can be created from a Plan using Plan.Context.
|
||||||
|
type Context struct {
|
||||||
|
config *config.Config
|
||||||
|
diff *Diff
|
||||||
|
hooks []Hook
|
||||||
|
state *State
|
||||||
|
providers map[string]ResourceProviderFactory
|
||||||
|
variables map[string]string
|
||||||
|
|
||||||
|
l sync.Mutex
|
||||||
|
runCh <-chan struct{}
|
||||||
|
sh *stopHook
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextOpts are the user-creatable configuration structure to create
|
||||||
|
// a context with NewContext.
|
||||||
|
type ContextOpts struct {
|
||||||
|
Config *config.Config
|
||||||
|
Diff *Diff
|
||||||
|
Hooks []Hook
|
||||||
|
State *State
|
||||||
|
Providers map[string]ResourceProviderFactory
|
||||||
|
Variables map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContext creates a new context.
|
||||||
|
//
|
||||||
|
// Once a context is created, the pointer values within ContextOpts should
|
||||||
|
// not be mutated in any way, since the pointers are copied, not the values
|
||||||
|
// themselves.
|
||||||
|
func NewContext(opts *ContextOpts) *Context {
|
||||||
|
sh := new(stopHook)
|
||||||
|
|
||||||
|
// Copy all the hooks and add our stop hook. We don't append directly
|
||||||
|
// to the Config so that we're not modifying that in-place.
|
||||||
|
hooks := make([]Hook, len(opts.Hooks)+1)
|
||||||
|
copy(hooks, opts.Hooks)
|
||||||
|
hooks[len(opts.Hooks)] = sh
|
||||||
|
|
||||||
|
return &Context{
|
||||||
|
config: opts.Config,
|
||||||
|
diff: opts.Diff,
|
||||||
|
hooks: hooks,
|
||||||
|
state: opts.State,
|
||||||
|
providers: opts.Providers,
|
||||||
|
variables: opts.Variables,
|
||||||
|
|
||||||
|
sh: sh,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply applies the changes represented by this context and returns
|
||||||
|
// the resulting state.
|
||||||
|
//
|
||||||
|
// In addition to returning the resulting state, this context is updated
|
||||||
|
// with the latest state.
|
||||||
|
func (c *Context) Apply() (*State, error) {
|
||||||
|
v := c.acquireRun()
|
||||||
|
defer c.releaseRun(v)
|
||||||
|
|
||||||
|
g, err := Graph(&GraphOpts{
|
||||||
|
Config: c.config,
|
||||||
|
Diff: c.diff,
|
||||||
|
Providers: c.providers,
|
||||||
|
State: c.state,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create our result. Make sure we preserve the prior states
|
||||||
|
s := new(State)
|
||||||
|
s.init()
|
||||||
|
if c.state != nil {
|
||||||
|
for k, v := range c.state.Resources {
|
||||||
|
s.Resources[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk
|
||||||
|
err = g.Walk(c.applyWalkFn(s))
|
||||||
|
|
||||||
|
// Update our state, even if we have an error, for partial updates
|
||||||
|
c.state = s
|
||||||
|
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan generates an execution plan for the given context.
|
||||||
|
//
|
||||||
|
// The execution plan encapsulates the context and can be stored
|
||||||
|
// in order to reinstantiate a context later for Apply.
|
||||||
|
//
|
||||||
|
// Plan also updates the diff of this context to be the diff generated
|
||||||
|
// by the plan, so Apply can be called after.
|
||||||
|
func (c *Context) Plan(opts *PlanOpts) (*Plan, error) {
|
||||||
|
v := c.acquireRun()
|
||||||
|
defer c.releaseRun(v)
|
||||||
|
|
||||||
|
g, err := Graph(&GraphOpts{
|
||||||
|
Config: c.config,
|
||||||
|
Providers: c.providers,
|
||||||
|
State: c.state,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &Plan{
|
||||||
|
Config: c.config,
|
||||||
|
Vars: c.variables,
|
||||||
|
State: c.state,
|
||||||
|
}
|
||||||
|
err = g.Walk(c.planWalkFn(p, opts))
|
||||||
|
|
||||||
|
// Update the diff so that our context is up-to-date
|
||||||
|
c.diff = p.Diff
|
||||||
|
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh goes through all the resources in the state and refreshes them
|
||||||
|
// to their latest state. This will update the state that this context
|
||||||
|
// works with, along with returning it.
|
||||||
|
//
|
||||||
|
// Even in the case an error is returned, the state will be returned and
|
||||||
|
// will potentially be partially updated.
|
||||||
|
func (c *Context) Refresh() (*State, error) {
|
||||||
|
v := c.acquireRun()
|
||||||
|
defer c.releaseRun(v)
|
||||||
|
|
||||||
|
g, err := Graph(&GraphOpts{
|
||||||
|
Config: c.config,
|
||||||
|
Providers: c.providers,
|
||||||
|
State: c.state,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.state, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s := new(State)
|
||||||
|
s.init()
|
||||||
|
err = g.Walk(c.refreshWalkFn(s))
|
||||||
|
|
||||||
|
// Update our state
|
||||||
|
c.state = s
|
||||||
|
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the running task.
|
||||||
|
//
|
||||||
|
// Stop will block until the task completes.
|
||||||
|
func (c *Context) Stop() {
|
||||||
|
c.l.Lock()
|
||||||
|
ch := c.runCh
|
||||||
|
|
||||||
|
// If we aren't running, then just return
|
||||||
|
if ch == nil {
|
||||||
|
c.l.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the hook we want to stop
|
||||||
|
c.sh.Stop()
|
||||||
|
|
||||||
|
// Wait for us to stop
|
||||||
|
c.l.Unlock()
|
||||||
|
<-ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the configuration and returns any warnings or errors.
|
||||||
|
func (c *Context) Validate() ([]string, []error) {
|
||||||
|
var rerr *multierror.Error
|
||||||
|
|
||||||
|
// Validate the configuration itself
|
||||||
|
if err := c.config.Validate(); err != nil {
|
||||||
|
rerr = multierror.ErrorAppend(rerr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the user variables
|
||||||
|
if errs := smcUserVariables(c.config, c.variables); len(errs) > 0 {
|
||||||
|
rerr = multierror.ErrorAppend(rerr, errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the graph
|
||||||
|
g, err := c.graph()
|
||||||
|
if err != nil {
|
||||||
|
rerr = multierror.ErrorAppend(rerr, fmt.Errorf(
|
||||||
|
"Error creating graph: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the graph and validate all the configs
|
||||||
|
var warns []string
|
||||||
|
var errs []error
|
||||||
|
err = g.Walk(c.validateWalkFn(&warns, &errs))
|
||||||
|
if err != nil {
|
||||||
|
rerr = multierror.ErrorAppend(rerr, fmt.Errorf(
|
||||||
|
"Error validating resources in graph: %s", err))
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
rerr = multierror.ErrorAppend(rerr, errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
errs = nil
|
||||||
|
if rerr != nil && len(rerr.Errors) > 0 {
|
||||||
|
errs = rerr.Errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return warns, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) graph() (*depgraph.Graph, error) {
|
||||||
|
return Graph(&GraphOpts{
|
||||||
|
Config: c.config,
|
||||||
|
Diff: c.diff,
|
||||||
|
Providers: c.providers,
|
||||||
|
State: c.state,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) acquireRun() chan<- struct{} {
|
||||||
|
c.l.Lock()
|
||||||
|
defer c.l.Unlock()
|
||||||
|
|
||||||
|
// Wait for no channel to exist
|
||||||
|
for c.runCh != nil {
|
||||||
|
c.l.Unlock()
|
||||||
|
ch := c.runCh
|
||||||
|
<-ch
|
||||||
|
c.l.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := make(chan struct{})
|
||||||
|
c.runCh = ch
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) releaseRun(ch chan<- struct{}) {
|
||||||
|
c.l.Lock()
|
||||||
|
defer c.l.Unlock()
|
||||||
|
|
||||||
|
close(ch)
|
||||||
|
c.runCh = nil
|
||||||
|
c.sh.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) applyWalkFn(result *State) depgraph.WalkFunc {
|
||||||
|
var l sync.Mutex
|
||||||
|
|
||||||
|
// Initialize the result
|
||||||
|
result.init()
|
||||||
|
|
||||||
|
cb := func(r *Resource) (map[string]string, error) {
|
||||||
|
diff := r.Diff
|
||||||
|
if diff.Empty() {
|
||||||
|
return r.Vars(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !diff.Destroy {
|
||||||
|
var err error
|
||||||
|
diff, err = r.Provider.Diff(r.State, r.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(mitchellh): we need to verify the diff doesn't change
|
||||||
|
// anything and that the diff has no computed values (pre-computed)
|
||||||
|
|
||||||
|
for _, h := range c.hooks {
|
||||||
|
handleHook(h.PreApply(r.Id, r.State, diff))
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the completed diff, apply!
|
||||||
|
log.Printf("[DEBUG] %s: Executing Apply", r.Id)
|
||||||
|
rs, err := r.Provider.Apply(r.State, diff)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the result is instantiated
|
||||||
|
if rs == nil {
|
||||||
|
rs = new(ResourceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force the resource state type to be our type
|
||||||
|
rs.Type = r.State.Type
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
for ak, av := range rs.Attributes {
|
||||||
|
// If the value is the unknown variable value, then it is an error.
|
||||||
|
// In this case we record the error and remove it from the state
|
||||||
|
if av == config.UnknownVariableValue {
|
||||||
|
errs = append(errs, fmt.Errorf(
|
||||||
|
"Attribute with unknown value: %s", ak))
|
||||||
|
delete(rs.Attributes, ak)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the resulting diff
|
||||||
|
l.Lock()
|
||||||
|
if rs.ID == "" {
|
||||||
|
delete(result.Resources, r.Id)
|
||||||
|
} else {
|
||||||
|
result.Resources[r.Id] = rs
|
||||||
|
}
|
||||||
|
l.Unlock()
|
||||||
|
|
||||||
|
// Update the state for the resource itself
|
||||||
|
r.State = rs
|
||||||
|
|
||||||
|
for _, h := range c.hooks {
|
||||||
|
handleHook(h.PostApply(r.Id, r.State))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the new state and update variables
|
||||||
|
err = nil
|
||||||
|
if len(errs) > 0 {
|
||||||
|
err = &multierror.Error{Errors: errs}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Vars(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.genericWalkFn(c.variables, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) planWalkFn(result *Plan, opts *PlanOpts) depgraph.WalkFunc {
|
||||||
|
var l sync.Mutex
|
||||||
|
|
||||||
|
// If we were given nil options, instantiate it
|
||||||
|
if opts == nil {
|
||||||
|
opts = new(PlanOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the result
|
||||||
|
result.init()
|
||||||
|
|
||||||
|
cb := func(r *Resource) (map[string]string, error) {
|
||||||
|
var diff *ResourceDiff
|
||||||
|
|
||||||
|
for _, h := range c.hooks {
|
||||||
|
handleHook(h.PreDiff(r.Id, r.State))
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Destroy {
|
||||||
|
if r.State.ID != "" {
|
||||||
|
log.Printf("[DEBUG] %s: Making for destroy", r.Id)
|
||||||
|
diff = &ResourceDiff{Destroy: true}
|
||||||
|
} else {
|
||||||
|
log.Printf("[DEBUG] %s: Not marking for destroy, no ID", r.Id)
|
||||||
|
}
|
||||||
|
} else if r.Config == nil {
|
||||||
|
log.Printf("[DEBUG] %s: Orphan, marking for destroy", r.Id)
|
||||||
|
|
||||||
|
// This is an orphan (no config), so we mark it to be destroyed
|
||||||
|
diff = &ResourceDiff{Destroy: true}
|
||||||
|
} else {
|
||||||
|
log.Printf("[DEBUG] %s: Executing diff", r.Id)
|
||||||
|
|
||||||
|
// Get a diff from the newest state
|
||||||
|
var err error
|
||||||
|
diff, err = r.Provider.Diff(r.State, r.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Lock()
|
||||||
|
if !diff.Empty() {
|
||||||
|
result.Diff.Resources[r.Id] = diff
|
||||||
|
}
|
||||||
|
l.Unlock()
|
||||||
|
|
||||||
|
for _, h := range c.hooks {
|
||||||
|
handleHook(h.PostDiff(r.Id, diff))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the new state and update variables
|
||||||
|
if !diff.Empty() {
|
||||||
|
r.State = r.State.MergeDiff(diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Vars(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.genericWalkFn(c.variables, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) refreshWalkFn(result *State) depgraph.WalkFunc {
|
||||||
|
var l sync.Mutex
|
||||||
|
|
||||||
|
cb := func(r *Resource) (map[string]string, error) {
|
||||||
|
for _, h := range c.hooks {
|
||||||
|
handleHook(h.PreRefresh(r.Id, r.State))
|
||||||
|
}
|
||||||
|
|
||||||
|
rs, err := r.Provider.Refresh(r.State)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if rs == nil {
|
||||||
|
rs = new(ResourceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix the type to be the type we have
|
||||||
|
rs.Type = r.State.Type
|
||||||
|
|
||||||
|
l.Lock()
|
||||||
|
result.Resources[r.Id] = rs
|
||||||
|
l.Unlock()
|
||||||
|
|
||||||
|
for _, h := range c.hooks {
|
||||||
|
handleHook(h.PostRefresh(r.Id, rs))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.genericWalkFn(c.variables, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) validateWalkFn(rws *[]string, res *[]error) depgraph.WalkFunc {
|
||||||
|
return func(n *depgraph.Noun) error {
|
||||||
|
// If it is the root node, ignore
|
||||||
|
if n.Name == GraphRootNode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rn := n.Meta.(type) {
|
||||||
|
case *GraphNodeResource:
|
||||||
|
if rn.Resource == nil {
|
||||||
|
panic("resource should never be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it doesn't have a provider, that is a different problem
|
||||||
|
if rn.Resource.Provider == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[INFO] Validating resource: %s", rn.Resource.Id)
|
||||||
|
ws, es := rn.Resource.Provider.ValidateResource(
|
||||||
|
rn.Type, rn.Resource.Config)
|
||||||
|
for i, w := range ws {
|
||||||
|
ws[i] = fmt.Sprintf("'%s' warning: %s", rn.Resource.Id, w)
|
||||||
|
}
|
||||||
|
for i, e := range es {
|
||||||
|
es[i] = fmt.Errorf("'%s' error: %s", rn.Resource.Id, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
*rws = append(*rws, ws...)
|
||||||
|
*res = append(*res, es...)
|
||||||
|
case *GraphNodeResourceProvider:
|
||||||
|
if rn.Config == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rc := NewResourceConfig(rn.Config.RawConfig)
|
||||||
|
|
||||||
|
for k, p := range rn.Providers {
|
||||||
|
log.Printf("[INFO] Validating provider: %s", k)
|
||||||
|
ws, es := p.Validate(rc)
|
||||||
|
for i, w := range ws {
|
||||||
|
ws[i] = fmt.Sprintf("Provider '%s' warning: %s", k, w)
|
||||||
|
}
|
||||||
|
for i, e := range es {
|
||||||
|
es[i] = fmt.Errorf("Provider '%s' error: %s", k, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
*rws = append(*rws, ws...)
|
||||||
|
*res = append(*res, es...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) genericWalkFn(
|
||||||
|
invars map[string]string,
|
||||||
|
cb genericWalkFunc) depgraph.WalkFunc {
|
||||||
|
var l sync.RWMutex
|
||||||
|
|
||||||
|
// Initialize the variables for application
|
||||||
|
vars := make(map[string]string)
|
||||||
|
for k, v := range invars {
|
||||||
|
vars[fmt.Sprintf("var.%s", k)] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will keep track of whether we're stopped or not
|
||||||
|
var stop uint32 = 0
|
||||||
|
|
||||||
|
return func(n *depgraph.Noun) error {
|
||||||
|
// If it is the root node, ignore
|
||||||
|
if n.Name == GraphRootNode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're stopped, return right away
|
||||||
|
if atomic.LoadUint32(&stop) != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m := n.Meta.(type) {
|
||||||
|
case *GraphNodeResource:
|
||||||
|
case *GraphNodeResourceProvider:
|
||||||
|
var rc *ResourceConfig
|
||||||
|
if m.Config != nil {
|
||||||
|
if err := m.Config.RawConfig.Interpolate(vars); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
rc = NewResourceConfig(m.Config.RawConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, p := range m.Providers {
|
||||||
|
log.Printf("[INFO] Configuring provider: %s", k)
|
||||||
|
err := p.Configure(rc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rn := n.Meta.(*GraphNodeResource)
|
||||||
|
|
||||||
|
l.RLock()
|
||||||
|
if len(vars) > 0 && rn.Config != nil {
|
||||||
|
if err := rn.Config.RawConfig.Interpolate(vars); err != nil {
|
||||||
|
panic(fmt.Sprintf("Interpolate error: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force the config to be set later
|
||||||
|
rn.Resource.Config = nil
|
||||||
|
}
|
||||||
|
l.RUnlock()
|
||||||
|
|
||||||
|
// Make sure that at least some resource configuration is set
|
||||||
|
if !rn.Orphan {
|
||||||
|
if rn.Resource.Config == nil {
|
||||||
|
if rn.Config == nil {
|
||||||
|
rn.Resource.Config = new(ResourceConfig)
|
||||||
|
} else {
|
||||||
|
rn.Resource.Config = NewResourceConfig(rn.Config.RawConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rn.Resource.Config = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle recovery of special panic scenarios
|
||||||
|
defer func() {
|
||||||
|
if v := recover(); v != nil {
|
||||||
|
if v == HookActionHalt {
|
||||||
|
atomic.StoreUint32(&stop, 1)
|
||||||
|
} else {
|
||||||
|
panic(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Call the callack
|
||||||
|
log.Printf("[INFO] Walking: %s", rn.Resource.Id)
|
||||||
|
newVars, err := cb(rn.Resource)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newVars) > 0 {
|
||||||
|
// Acquire a lock since this function is called in parallel
|
||||||
|
l.Lock()
|
||||||
|
defer l.Unlock()
|
||||||
|
|
||||||
|
// Update variables
|
||||||
|
for k, v := range newVars {
|
||||||
|
vars[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
"github.com/hashicorp/terraform/depgraph"
|
"github.com/hashicorp/terraform/depgraph"
|
||||||
|
"github.com/hashicorp/terraform/helper/multierror"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GraphOpts are options used to create the resource graph that Terraform
|
// GraphOpts are options used to create the resource graph that Terraform
|
||||||
|
@ -325,7 +326,7 @@ func graphAddMissingResourceProviders(
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return &MultiError{Errors: errs}
|
return &multierror.Error{Errors: errs}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -518,7 +519,7 @@ func graphInitResourceProviders(
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return &MultiError{Errors: errs}
|
return &multierror.Error{Errors: errs}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -583,7 +584,7 @@ func graphMapResourceProviders(g *depgraph.Graph) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return &MultiError{Errors: errs}
|
return &multierror.Error{Errors: errs}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -1,24 +1,13 @@
|
||||||
package terraform
|
package terraform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
// stopHook is a private Hook implementation that Terraform uses to
|
// stopHook is a private Hook implementation that Terraform uses to
|
||||||
// signal when to stop or cancel actions.
|
// signal when to stop or cancel actions.
|
||||||
type stopHook struct {
|
type stopHook struct {
|
||||||
sync.Mutex
|
stop uint32
|
||||||
|
|
||||||
// This should be incremented for every thing that can be stopped.
|
|
||||||
// When this is zero, a stopper can assume that everything is properly
|
|
||||||
// stopped.
|
|
||||||
count int
|
|
||||||
|
|
||||||
// This channel should be closed when it is time to stop
|
|
||||||
ch chan struct{}
|
|
||||||
|
|
||||||
serial int
|
|
||||||
stoppedCh chan<- struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *stopHook) PreApply(string, *ResourceState, *ResourceDiff) (HookAction, error) {
|
func (h *stopHook) PreApply(string, *ResourceState, *ResourceDiff) (HookAction, error) {
|
||||||
|
@ -46,34 +35,22 @@ func (h *stopHook) PostRefresh(string, *ResourceState) (HookAction, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *stopHook) hook() (HookAction, error) {
|
func (h *stopHook) hook() (HookAction, error) {
|
||||||
select {
|
if h.Stopped() {
|
||||||
case <-h.ch:
|
|
||||||
h.stoppedCh <- struct{}{}
|
|
||||||
return HookActionHalt, nil
|
return HookActionHalt, nil
|
||||||
default:
|
|
||||||
return HookActionContinue, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return HookActionContinue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset should be called within the lock context
|
// reset should be called within the lock context
|
||||||
func (h *stopHook) reset() {
|
func (h *stopHook) Reset() {
|
||||||
h.ch = make(chan struct{})
|
atomic.StoreUint32(&h.stop, 0)
|
||||||
h.count = 0
|
|
||||||
h.serial += 1
|
|
||||||
h.stoppedCh = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *stopHook) ref() int {
|
func (h *stopHook) Stop() {
|
||||||
h.Lock()
|
atomic.StoreUint32(&h.stop, 1)
|
||||||
defer h.Unlock()
|
|
||||||
h.count++
|
|
||||||
return h.serial
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *stopHook) unref(s int) {
|
func (h *stopHook) Stopped() bool {
|
||||||
h.Lock()
|
return atomic.LoadUint32(&h.stop) == 1
|
||||||
defer h.Unlock()
|
|
||||||
if h.serial == s {
|
|
||||||
h.count--
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,6 @@ type PlanOpts struct {
|
||||||
// that are created. Otherwise, it will move towards the desired state
|
// that are created. Otherwise, it will move towards the desired state
|
||||||
// specified in the configuration.
|
// specified in the configuration.
|
||||||
Destroy bool
|
Destroy bool
|
||||||
|
|
||||||
Config *config.Config
|
|
||||||
State *State
|
|
||||||
Vars map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plan represents a single Terraform execution plan, which contains
|
// Plan represents a single Terraform execution plan, which contains
|
||||||
|
@ -40,6 +36,18 @@ type Plan struct {
|
||||||
once sync.Once
|
once sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Context returns a Context with the data encapsulated in this plan.
|
||||||
|
//
|
||||||
|
// The following fields in opts are overridden by the plan: Config,
|
||||||
|
// Diff, State, Variables.
|
||||||
|
func (p *Plan) Context(opts *ContextOpts) *Context {
|
||||||
|
opts.Config = p.Config
|
||||||
|
opts.Diff = p.Diff
|
||||||
|
opts.State = p.State
|
||||||
|
opts.Variables = p.Vars
|
||||||
|
return NewContext(opts)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Plan) String() string {
|
func (p *Plan) String() string {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
buf.WriteString("DIFF:\n\n")
|
buf.WriteString("DIFF:\n\n")
|
||||||
|
|
|
@ -17,14 +17,31 @@ type ResourceProvider interface {
|
||||||
// (no interpolation done) and can return a list of warnings and/or
|
// (no interpolation done) and can return a list of warnings and/or
|
||||||
// errors.
|
// 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.
|
// 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
|
// The primary use case of this call is to check that required keys are
|
||||||
// set.
|
// set.
|
||||||
Validate(*ResourceConfig) ([]string, []error)
|
Validate(*ResourceConfig) ([]string, []error)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
// Configure configures the provider itself with the configuration
|
// Configure configures the provider itself with the configuration
|
||||||
// given. This is useful for setting things like access keys.
|
// 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 returns an error if it occurred.
|
||||||
Configure(*ResourceConfig) error
|
Configure(*ResourceConfig) error
|
||||||
|
|
||||||
|
|
|
@ -6,32 +6,37 @@ type MockResourceProvider struct {
|
||||||
// Anything you want, in case you need to store extra data with the mock.
|
// Anything you want, in case you need to store extra data with the mock.
|
||||||
Meta interface{}
|
Meta interface{}
|
||||||
|
|
||||||
ApplyCalled bool
|
ApplyCalled bool
|
||||||
ApplyState *ResourceState
|
ApplyState *ResourceState
|
||||||
ApplyDiff *ResourceDiff
|
ApplyDiff *ResourceDiff
|
||||||
ApplyFn func(*ResourceState, *ResourceDiff) (*ResourceState, error)
|
ApplyFn func(*ResourceState, *ResourceDiff) (*ResourceState, error)
|
||||||
ApplyReturn *ResourceState
|
ApplyReturn *ResourceState
|
||||||
ApplyReturnError error
|
ApplyReturnError error
|
||||||
ConfigureCalled bool
|
ConfigureCalled bool
|
||||||
ConfigureConfig *ResourceConfig
|
ConfigureConfig *ResourceConfig
|
||||||
ConfigureReturnError error
|
ConfigureReturnError error
|
||||||
DiffCalled bool
|
DiffCalled bool
|
||||||
DiffState *ResourceState
|
DiffState *ResourceState
|
||||||
DiffDesired *ResourceConfig
|
DiffDesired *ResourceConfig
|
||||||
DiffFn func(*ResourceState, *ResourceConfig) (*ResourceDiff, error)
|
DiffFn func(*ResourceState, *ResourceConfig) (*ResourceDiff, error)
|
||||||
DiffReturn *ResourceDiff
|
DiffReturn *ResourceDiff
|
||||||
DiffReturnError error
|
DiffReturnError error
|
||||||
RefreshCalled bool
|
RefreshCalled bool
|
||||||
RefreshState *ResourceState
|
RefreshState *ResourceState
|
||||||
RefreshFn func(*ResourceState) (*ResourceState, error)
|
RefreshFn func(*ResourceState) (*ResourceState, error)
|
||||||
RefreshReturn *ResourceState
|
RefreshReturn *ResourceState
|
||||||
RefreshReturnError error
|
RefreshReturnError error
|
||||||
ResourcesCalled bool
|
ResourcesCalled bool
|
||||||
ResourcesReturn []ResourceType
|
ResourcesReturn []ResourceType
|
||||||
ValidateCalled bool
|
ValidateCalled bool
|
||||||
ValidateConfig *ResourceConfig
|
ValidateConfig *ResourceConfig
|
||||||
ValidateReturnWarns []string
|
ValidateReturnWarns []string
|
||||||
ValidateReturnErrors []error
|
ValidateReturnErrors []error
|
||||||
|
ValidateResourceCalled bool
|
||||||
|
ValidateResourceType string
|
||||||
|
ValidateResourceConfig *ResourceConfig
|
||||||
|
ValidateResourceReturnWarns []string
|
||||||
|
ValidateResourceReturnErrors []error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MockResourceProvider) Validate(c *ResourceConfig) ([]string, []error) {
|
func (p *MockResourceProvider) Validate(c *ResourceConfig) ([]string, []error) {
|
||||||
|
@ -40,6 +45,13 @@ func (p *MockResourceProvider) Validate(c *ResourceConfig) ([]string, []error) {
|
||||||
return p.ValidateReturnWarns, p.ValidateReturnErrors
|
return p.ValidateReturnWarns, p.ValidateReturnErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *MockResourceProvider) ValidateResource(t string, c *ResourceConfig) ([]string, []error) {
|
||||||
|
p.ValidateResourceCalled = true
|
||||||
|
p.ValidateResourceType = t
|
||||||
|
p.ValidateResourceConfig = c
|
||||||
|
return p.ValidateResourceReturnWarns, p.ValidateResourceReturnErrors
|
||||||
|
}
|
||||||
|
|
||||||
func (p *MockResourceProvider) Configure(c *ResourceConfig) error {
|
func (p *MockResourceProvider) Configure(c *ResourceConfig) error {
|
||||||
p.ConfigureCalled = true
|
p.ConfigureCalled = true
|
||||||
p.ConfigureConfig = c
|
p.ConfigureConfig = c
|
||||||
|
|
|
@ -1,147 +1,24 @@
|
||||||
package terraform
|
package terraform
|
||||||
|
|
||||||
/*
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
// smcUserVariables does all the semantic checks to verify that the
|
||||||
// smcProviders matches up the resources with a provider and initializes
|
// variables given satisfy the configuration itself.
|
||||||
// it. This does not call "Configure" on the ResourceProvider, since that
|
func smcUserVariables(c *config.Config, vs map[string]string) []error {
|
||||||
// might actually depend on upstream resources.
|
|
||||||
func smcProviders(
|
|
||||||
c *Config) (map[*config.Resource]*terraformProvider, []error) {
|
|
||||||
var errs []error
|
|
||||||
|
|
||||||
// Keep track of providers we know we couldn't instantiate so
|
|
||||||
// that we don't get a ton of errors about the same provider.
|
|
||||||
failures := make(map[string]struct{})
|
|
||||||
|
|
||||||
// Go through each resource and match it up to a provider
|
|
||||||
mapping := make(map[*config.Resource]*terraformProvider)
|
|
||||||
providers := make(map[string]ResourceProvider)
|
|
||||||
tpcache := make(map[string]*terraformProvider)
|
|
||||||
|
|
||||||
ResourceLoop:
|
|
||||||
for _, r := range c.Config.Resources {
|
|
||||||
// Find the prefixes that match this in the order of
|
|
||||||
// longest matching first (most specific)
|
|
||||||
prefixes := matchingPrefixes(r.Type, c.Providers)
|
|
||||||
if len(prefixes) > 0 {
|
|
||||||
if _, ok := failures[prefixes[0]]; ok {
|
|
||||||
// We already failed this provider, meaning this
|
|
||||||
// resource will never succeed, so just continue.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go through each prefix and instantiate if necessary, then
|
|
||||||
// verify if this provider is of use to us or not.
|
|
||||||
var providerName string
|
|
||||||
var provider ResourceProvider
|
|
||||||
for _, prefix := range prefixes {
|
|
||||||
// Initialize the provider
|
|
||||||
p, ok := providers[prefix]
|
|
||||||
if !ok {
|
|
||||||
var err error
|
|
||||||
p, err = c.Providers[prefix]()
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf(
|
|
||||||
"Error instantiating resource provider for "+
|
|
||||||
"prefix %s: %s", prefix, err))
|
|
||||||
|
|
||||||
// Record the error so that we don't check it again
|
|
||||||
failures[prefix] = struct{}{}
|
|
||||||
|
|
||||||
// Jump to the next resource
|
|
||||||
continue ResourceLoop
|
|
||||||
}
|
|
||||||
|
|
||||||
providers[prefix] = p
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test if this provider matches what we need
|
|
||||||
if !ProviderSatisfies(p, r.Type) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
providerName = prefix
|
|
||||||
provider = p
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we didn't find a valid provider, then error and continue
|
|
||||||
if providerName == "" {
|
|
||||||
errs = append(errs, fmt.Errorf(
|
|
||||||
"Provider for resource %s not found.",
|
|
||||||
r.Id()))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the matching provider configuration for this resource
|
|
||||||
var pc *config.ProviderConfig
|
|
||||||
pcName := config.ProviderConfigName(r.Type, c.Config.ProviderConfigs)
|
|
||||||
if pcName != "" {
|
|
||||||
pc = c.Config.ProviderConfigs[pcName]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look up if we already have a provider for this pair of PC
|
|
||||||
// and provider name. If not, create it.
|
|
||||||
cacheKey := fmt.Sprintf("%s|%s", pcName, providerName)
|
|
||||||
tp, ok := tpcache[cacheKey]
|
|
||||||
if !ok {
|
|
||||||
renew := false
|
|
||||||
for _, tp := range tpcache {
|
|
||||||
if tp.Provider == provider {
|
|
||||||
renew = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if renew {
|
|
||||||
var err error
|
|
||||||
provider, err = c.Providers[providerName]()
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf(
|
|
||||||
"Error instantiating resource provider for "+
|
|
||||||
"prefix %s: %s", providerName, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tp = &terraformProvider{
|
|
||||||
Provider: provider,
|
|
||||||
Config: pc,
|
|
||||||
}
|
|
||||||
tpcache[cacheKey] = tp
|
|
||||||
}
|
|
||||||
|
|
||||||
mapping[r] = tp
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(errs) > 0 {
|
|
||||||
return nil, errs
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapping, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// smcVariables does all the semantic checks to verify that the
|
|
||||||
// variables given in the configuration to instantiate a Terraform
|
|
||||||
// struct are valid.
|
|
||||||
func smcVariables(c *Config) []error {
|
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|
||||||
// Check that all required variables are present
|
// Check that all required variables are present
|
||||||
required := make(map[string]struct{})
|
required := make(map[string]struct{})
|
||||||
for k, v := range c.Config.Variables {
|
for k, v := range c.Variables {
|
||||||
if v.Required() {
|
if v.Required() {
|
||||||
required[k] = struct{}{}
|
required[k] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k, _ := range c.Variables {
|
for k, _ := range vs {
|
||||||
delete(required, k)
|
delete(required, k)
|
||||||
}
|
}
|
||||||
if len(required) > 0 {
|
if len(required) > 0 {
|
||||||
|
@ -155,4 +32,3 @@ func smcVariables(c *Config) []error {
|
||||||
|
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSMCUserVariables(t *testing.T) {
|
||||||
|
c := testConfig(t, "smc-uservars")
|
||||||
|
|
||||||
|
// Required variables not set
|
||||||
|
errs := smcUserVariables(c, nil)
|
||||||
|
if len(errs) == 0 {
|
||||||
|
t.Fatal("should have errors")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required variables set, optional variables unset
|
||||||
|
errs = smcUserVariables(c, map[string]string{"foo": "bar"})
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Fatalf("err: %#v", errs)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,455 +0,0 @@
|
||||||
package terraform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/config"
|
|
||||||
"github.com/hashicorp/terraform/depgraph"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Terraform is the primary structure that is used to interact with
|
|
||||||
// Terraform from code, and can perform operations such as returning
|
|
||||||
// all resources, a resource tree, a specific resource, etc.
|
|
||||||
type Terraform struct {
|
|
||||||
hooks []Hook
|
|
||||||
providers map[string]ResourceProviderFactory
|
|
||||||
stopHook *stopHook
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a function type used to implement a walker for the resource
|
|
||||||
// tree internally on the Terraform structure.
|
|
||||||
type genericWalkFunc func(*Resource) (map[string]string, error)
|
|
||||||
|
|
||||||
// Config is the configuration that must be given to instantiate
|
|
||||||
// a Terraform structure.
|
|
||||||
type Config struct {
|
|
||||||
Hooks []Hook
|
|
||||||
Providers map[string]ResourceProviderFactory
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new Terraform structure, initializes resource providers
|
|
||||||
// for the given configuration, etc.
|
|
||||||
//
|
|
||||||
// Semantic checks of the entire configuration structure are done at this
|
|
||||||
// time, as well as richer checks such as verifying that the resource providers
|
|
||||||
// can be properly initialized, can be configured, etc.
|
|
||||||
func New(c *Config) (*Terraform, error) {
|
|
||||||
sh := new(stopHook)
|
|
||||||
sh.Lock()
|
|
||||||
sh.reset()
|
|
||||||
sh.Unlock()
|
|
||||||
|
|
||||||
// Copy all the hooks and add our stop hook. We don't append directly
|
|
||||||
// to the Config so that we're not modifying that in-place.
|
|
||||||
hooks := make([]Hook, len(c.Hooks)+1)
|
|
||||||
copy(hooks, c.Hooks)
|
|
||||||
hooks[len(c.Hooks)] = sh
|
|
||||||
|
|
||||||
return &Terraform{
|
|
||||||
hooks: hooks,
|
|
||||||
stopHook: sh,
|
|
||||||
providers: c.Providers,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Terraform) Apply(p *Plan) (*State, error) {
|
|
||||||
// Increase the count on the stop hook so we know when to stop
|
|
||||||
serial := t.stopHook.ref()
|
|
||||||
defer t.stopHook.unref(serial)
|
|
||||||
|
|
||||||
// Make sure we're working with a plan that doesn't have null pointers
|
|
||||||
// everywhere, and is instead just empty otherwise.
|
|
||||||
p.init()
|
|
||||||
|
|
||||||
g, err := Graph(&GraphOpts{
|
|
||||||
Config: p.Config,
|
|
||||||
Diff: p.Diff,
|
|
||||||
Providers: t.providers,
|
|
||||||
State: p.State,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.apply(g, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop stops all running tasks (applies, plans, refreshes).
|
|
||||||
//
|
|
||||||
// This will block until all running tasks are stopped. While Stop is
|
|
||||||
// blocked, any new calls to Apply, Plan, Refresh, etc. will also block. New
|
|
||||||
// calls, however, will start once this Stop has returned.
|
|
||||||
func (t *Terraform) Stop() {
|
|
||||||
log.Printf("[INFO] Terraform stopping tasks")
|
|
||||||
|
|
||||||
t.stopHook.Lock()
|
|
||||||
defer t.stopHook.Unlock()
|
|
||||||
|
|
||||||
// Setup the stoppedCh
|
|
||||||
stoppedCh := make(chan struct{}, t.stopHook.count)
|
|
||||||
t.stopHook.stoppedCh = stoppedCh
|
|
||||||
|
|
||||||
// Close the channel to signal that we're done
|
|
||||||
close(t.stopHook.ch)
|
|
||||||
|
|
||||||
// Expect the number of count stops...
|
|
||||||
log.Printf("[DEBUG] Waiting for %d tasks to stop", t.stopHook.count)
|
|
||||||
for i := 0; i < t.stopHook.count; i++ {
|
|
||||||
<-stoppedCh
|
|
||||||
}
|
|
||||||
log.Printf("[DEBUG] Stopped!")
|
|
||||||
|
|
||||||
// Success, everything stopped, reset everything
|
|
||||||
t.stopHook.reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Terraform) Plan(opts *PlanOpts) (*Plan, error) {
|
|
||||||
// Increase the count on the stop hook so we know when to stop
|
|
||||||
serial := t.stopHook.ref()
|
|
||||||
defer t.stopHook.unref(serial)
|
|
||||||
|
|
||||||
g, err := Graph(&GraphOpts{
|
|
||||||
Config: opts.Config,
|
|
||||||
Providers: t.providers,
|
|
||||||
State: opts.State,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.plan(g, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh goes through all the resources in the state and refreshes them
|
|
||||||
// to their latest status.
|
|
||||||
func (t *Terraform) Refresh(c *config.Config, s *State) (*State, error) {
|
|
||||||
// Increase the count on the stop hook so we know when to stop
|
|
||||||
serial := t.stopHook.ref()
|
|
||||||
defer t.stopHook.unref(serial)
|
|
||||||
|
|
||||||
g, err := Graph(&GraphOpts{
|
|
||||||
Config: c,
|
|
||||||
Providers: t.providers,
|
|
||||||
State: s,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.refresh(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Terraform) apply(
|
|
||||||
g *depgraph.Graph,
|
|
||||||
p *Plan) (*State, error) {
|
|
||||||
// Create our result. Make sure we preserve the prior states
|
|
||||||
s := new(State)
|
|
||||||
s.init()
|
|
||||||
for k, v := range p.State.Resources {
|
|
||||||
s.Resources[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
err := g.Walk(t.applyWalkFn(s, p))
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Terraform) plan(g *depgraph.Graph, opts *PlanOpts) (*Plan, error) {
|
|
||||||
p := &Plan{
|
|
||||||
Config: opts.Config,
|
|
||||||
Vars: opts.Vars,
|
|
||||||
State: opts.State,
|
|
||||||
}
|
|
||||||
err := g.Walk(t.planWalkFn(p, opts))
|
|
||||||
return p, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Terraform) refresh(g *depgraph.Graph) (*State, error) {
|
|
||||||
s := new(State)
|
|
||||||
err := g.Walk(t.refreshWalkFn(s))
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Terraform) refreshWalkFn(result *State) depgraph.WalkFunc {
|
|
||||||
var l sync.Mutex
|
|
||||||
|
|
||||||
// Initialize the result so we don't have to nil check everywhere
|
|
||||||
result.init()
|
|
||||||
|
|
||||||
cb := func(r *Resource) (map[string]string, error) {
|
|
||||||
for _, h := range t.hooks {
|
|
||||||
handleHook(h.PreRefresh(r.Id, r.State))
|
|
||||||
}
|
|
||||||
|
|
||||||
rs, err := r.Provider.Refresh(r.State)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if rs == nil {
|
|
||||||
rs = new(ResourceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix the type to be the type we have
|
|
||||||
rs.Type = r.State.Type
|
|
||||||
|
|
||||||
l.Lock()
|
|
||||||
result.Resources[r.Id] = rs
|
|
||||||
l.Unlock()
|
|
||||||
|
|
||||||
for _, h := range t.hooks {
|
|
||||||
handleHook(h.PostRefresh(r.Id, rs))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.genericWalkFn(nil, cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Terraform) applyWalkFn(
|
|
||||||
result *State,
|
|
||||||
p *Plan) depgraph.WalkFunc {
|
|
||||||
var l sync.Mutex
|
|
||||||
|
|
||||||
// Initialize the result
|
|
||||||
result.init()
|
|
||||||
|
|
||||||
cb := func(r *Resource) (map[string]string, error) {
|
|
||||||
diff := r.Diff
|
|
||||||
if diff.Empty() {
|
|
||||||
return r.Vars(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !diff.Destroy {
|
|
||||||
var err error
|
|
||||||
diff, err = r.Provider.Diff(r.State, r.Config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(mitchellh): we need to verify the diff doesn't change
|
|
||||||
// anything and that the diff has no computed values (pre-computed)
|
|
||||||
|
|
||||||
for _, h := range t.hooks {
|
|
||||||
handleHook(h.PreApply(r.Id, r.State, diff))
|
|
||||||
}
|
|
||||||
|
|
||||||
// With the completed diff, apply!
|
|
||||||
log.Printf("[DEBUG] %s: Executing Apply", r.Id)
|
|
||||||
rs, err := r.Provider.Apply(r.State, diff)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the result is instantiated
|
|
||||||
if rs == nil {
|
|
||||||
rs = new(ResourceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force the resource state type to be our type
|
|
||||||
rs.Type = r.State.Type
|
|
||||||
|
|
||||||
var errs []error
|
|
||||||
for ak, av := range rs.Attributes {
|
|
||||||
// If the value is the unknown variable value, then it is an error.
|
|
||||||
// In this case we record the error and remove it from the state
|
|
||||||
if av == config.UnknownVariableValue {
|
|
||||||
errs = append(errs, fmt.Errorf(
|
|
||||||
"Attribute with unknown value: %s", ak))
|
|
||||||
delete(rs.Attributes, ak)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the resulting diff
|
|
||||||
l.Lock()
|
|
||||||
if rs.ID == "" {
|
|
||||||
delete(result.Resources, r.Id)
|
|
||||||
} else {
|
|
||||||
result.Resources[r.Id] = rs
|
|
||||||
}
|
|
||||||
l.Unlock()
|
|
||||||
|
|
||||||
// Update the state for the resource itself
|
|
||||||
r.State = rs
|
|
||||||
|
|
||||||
for _, h := range t.hooks {
|
|
||||||
handleHook(h.PostApply(r.Id, r.State))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the new state and update variables
|
|
||||||
err = nil
|
|
||||||
if len(errs) > 0 {
|
|
||||||
err = &MultiError{Errors: errs}
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.Vars(), err
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.genericWalkFn(p.Vars, cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Terraform) planWalkFn(result *Plan, opts *PlanOpts) depgraph.WalkFunc {
|
|
||||||
var l sync.Mutex
|
|
||||||
|
|
||||||
// Initialize the result
|
|
||||||
result.init()
|
|
||||||
|
|
||||||
cb := func(r *Resource) (map[string]string, error) {
|
|
||||||
var diff *ResourceDiff
|
|
||||||
|
|
||||||
for _, h := range t.hooks {
|
|
||||||
handleHook(h.PreDiff(r.Id, r.State))
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.Destroy {
|
|
||||||
if r.State.ID != "" {
|
|
||||||
log.Printf("[DEBUG] %s: Making for destroy", r.Id)
|
|
||||||
diff = &ResourceDiff{Destroy: true}
|
|
||||||
} else {
|
|
||||||
log.Printf("[DEBUG] %s: Not marking for destroy, no ID", r.Id)
|
|
||||||
}
|
|
||||||
} else if r.Config == nil {
|
|
||||||
log.Printf("[DEBUG] %s: Orphan, marking for destroy", r.Id)
|
|
||||||
|
|
||||||
// This is an orphan (no config), so we mark it to be destroyed
|
|
||||||
diff = &ResourceDiff{Destroy: true}
|
|
||||||
} else {
|
|
||||||
log.Printf("[DEBUG] %s: Executing diff", r.Id)
|
|
||||||
|
|
||||||
// Get a diff from the newest state
|
|
||||||
var err error
|
|
||||||
diff, err = r.Provider.Diff(r.State, r.Config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Lock()
|
|
||||||
if !diff.Empty() {
|
|
||||||
result.Diff.Resources[r.Id] = diff
|
|
||||||
}
|
|
||||||
l.Unlock()
|
|
||||||
|
|
||||||
for _, h := range t.hooks {
|
|
||||||
handleHook(h.PostDiff(r.Id, diff))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the new state and update variables
|
|
||||||
if !diff.Empty() {
|
|
||||||
r.State = r.State.MergeDiff(diff)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.Vars(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.genericWalkFn(opts.Vars, cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Terraform) genericWalkFn(
|
|
||||||
invars map[string]string,
|
|
||||||
cb genericWalkFunc) depgraph.WalkFunc {
|
|
||||||
var l sync.RWMutex
|
|
||||||
|
|
||||||
// Initialize the variables for application
|
|
||||||
vars := make(map[string]string)
|
|
||||||
for k, v := range invars {
|
|
||||||
vars[fmt.Sprintf("var.%s", k)] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// This will keep track of whether we're stopped or not
|
|
||||||
var stop uint32 = 0
|
|
||||||
|
|
||||||
return func(n *depgraph.Noun) error {
|
|
||||||
// If it is the root node, ignore
|
|
||||||
if n.Name == GraphRootNode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're stopped, return right away
|
|
||||||
if atomic.LoadUint32(&stop) != 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch m := n.Meta.(type) {
|
|
||||||
case *GraphNodeResource:
|
|
||||||
case *GraphNodeResourceProvider:
|
|
||||||
var rc *ResourceConfig
|
|
||||||
if m.Config != nil {
|
|
||||||
if err := m.Config.RawConfig.Interpolate(vars); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
rc = NewResourceConfig(m.Config.RawConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, p := range m.Providers {
|
|
||||||
log.Printf("[INFO] Configuring provider: %s", k)
|
|
||||||
err := p.Configure(rc)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rn := n.Meta.(*GraphNodeResource)
|
|
||||||
|
|
||||||
l.RLock()
|
|
||||||
if len(vars) > 0 && rn.Config != nil {
|
|
||||||
if err := rn.Config.RawConfig.Interpolate(vars); err != nil {
|
|
||||||
panic(fmt.Sprintf("Interpolate error: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force the config to be set later
|
|
||||||
rn.Resource.Config = nil
|
|
||||||
}
|
|
||||||
l.RUnlock()
|
|
||||||
|
|
||||||
// Make sure that at least some resource configuration is set
|
|
||||||
if !rn.Orphan {
|
|
||||||
if rn.Resource.Config == nil {
|
|
||||||
if rn.Config == nil {
|
|
||||||
rn.Resource.Config = new(ResourceConfig)
|
|
||||||
} else {
|
|
||||||
rn.Resource.Config = NewResourceConfig(rn.Config.RawConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rn.Resource.Config = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle recovery of special panic scenarios
|
|
||||||
defer func() {
|
|
||||||
if v := recover(); v != nil {
|
|
||||||
if v == HookActionHalt {
|
|
||||||
atomic.StoreUint32(&stop, 1)
|
|
||||||
} else {
|
|
||||||
panic(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Call the callack
|
|
||||||
log.Printf("[INFO] Walking: %s", rn.Resource.Id)
|
|
||||||
newVars, err := cb(rn.Resource)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(newVars) > 0 {
|
|
||||||
// Acquire a lock since this function is called in parallel
|
|
||||||
l.Lock()
|
|
||||||
defer l.Unlock()
|
|
||||||
|
|
||||||
// Update variables
|
|
||||||
for k, v := range newVars {
|
|
||||||
vars[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,7 @@
|
||||||
package terraform
|
package terraform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -14,712 +11,6 @@ import (
|
||||||
// This is the directory where our test fixtures are.
|
// This is the directory where our test fixtures are.
|
||||||
const fixtureDir = "./test-fixtures"
|
const fixtureDir = "./test-fixtures"
|
||||||
|
|
||||||
func TestTerraformApply(t *testing.T) {
|
|
||||||
c := testConfig(t, "apply-good")
|
|
||||||
tf := testTerraform2(t, nil)
|
|
||||||
|
|
||||||
p, err := tf.Plan(&PlanOpts{Config: c})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := tf.Apply(p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(state.Resources) < 2 {
|
|
||||||
t.Fatalf("bad: %#v", state.Resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(state.String())
|
|
||||||
expected := strings.TrimSpace(testTerraformApplyStr)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad: \n%s", actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformApply_cancel(t *testing.T) {
|
|
||||||
stopped := false
|
|
||||||
stopCh := make(chan struct{})
|
|
||||||
stopReplyCh := make(chan struct{})
|
|
||||||
|
|
||||||
rpAWS := new(MockResourceProvider)
|
|
||||||
rpAWS.ResourcesReturn = []ResourceType{
|
|
||||||
ResourceType{Name: "aws_instance"},
|
|
||||||
}
|
|
||||||
rpAWS.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) {
|
|
||||||
return &ResourceDiff{
|
|
||||||
Attributes: map[string]*ResourceAttrDiff{
|
|
||||||
"num": &ResourceAttrDiff{
|
|
||||||
New: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
rpAWS.ApplyFn = func(*ResourceState, *ResourceDiff) (*ResourceState, error) {
|
|
||||||
if !stopped {
|
|
||||||
stopped = true
|
|
||||||
close(stopCh)
|
|
||||||
<-stopReplyCh
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ResourceState{
|
|
||||||
ID: "foo",
|
|
||||||
Attributes: map[string]string{
|
|
||||||
"num": "2",
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c := testConfig(t, "apply-cancel")
|
|
||||||
tf := testTerraform2(t, &Config{
|
|
||||||
Providers: map[string]ResourceProviderFactory{
|
|
||||||
"aws": testProviderFuncFixed(rpAWS),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
p, err := tf.Plan(&PlanOpts{Config: c})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the Apply in a goroutine
|
|
||||||
stateCh := make(chan *State)
|
|
||||||
go func() {
|
|
||||||
state, err := tf.Apply(p)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
stateCh <- state
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Start a goroutine so we can inject exactly when we stop
|
|
||||||
s := tf.stopHook.ref()
|
|
||||||
go func() {
|
|
||||||
defer tf.stopHook.unref(s)
|
|
||||||
<-tf.stopHook.ch
|
|
||||||
close(stopReplyCh)
|
|
||||||
tf.stopHook.stoppedCh <- struct{}{}
|
|
||||||
}()
|
|
||||||
|
|
||||||
<-stopCh
|
|
||||||
tf.Stop()
|
|
||||||
|
|
||||||
state := <-stateCh
|
|
||||||
|
|
||||||
if len(state.Resources) != 1 {
|
|
||||||
t.Fatalf("bad: %#v", state.Resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(state.String())
|
|
||||||
expected := strings.TrimSpace(testTerraformApplyCancelStr)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad: \n%s", actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformApply_compute(t *testing.T) {
|
|
||||||
// This tests that computed variables are properly re-diffed
|
|
||||||
// to get the value prior to application (Apply).
|
|
||||||
c := testConfig(t, "apply-compute")
|
|
||||||
tf := testTerraform2(t, nil)
|
|
||||||
|
|
||||||
p, err := tf.Plan(&PlanOpts{Config: c})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Vars["value"] = "1"
|
|
||||||
|
|
||||||
state, err := tf.Apply(p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(state.String())
|
|
||||||
expected := strings.TrimSpace(testTerraformApplyComputeStr)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad: \n%s", actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformApply_destroy(t *testing.T) {
|
|
||||||
h := new(HookRecordApplyOrder)
|
|
||||||
|
|
||||||
// First, apply the good configuration, build it
|
|
||||||
c := testConfig(t, "apply-destroy")
|
|
||||||
tf := testTerraform2(t, &Config{
|
|
||||||
Hooks: []Hook{h},
|
|
||||||
})
|
|
||||||
|
|
||||||
p, err := tf.Plan(&PlanOpts{Config: c})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := tf.Apply(p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, plan and apply a destroy operation
|
|
||||||
p, err = tf.Plan(&PlanOpts{
|
|
||||||
Config: new(config.Config),
|
|
||||||
State: state,
|
|
||||||
Destroy: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Active = true
|
|
||||||
|
|
||||||
state, err = tf.Apply(p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that things were destroyed
|
|
||||||
actual := strings.TrimSpace(state.String())
|
|
||||||
expected := strings.TrimSpace(testTerraformApplyDestroyStr)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad: \n%s", actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that things were destroyed _in the right order_
|
|
||||||
expected2 := []string{"aws_instance.bar", "aws_instance.foo"}
|
|
||||||
actual2 := h.IDs
|
|
||||||
if !reflect.DeepEqual(actual2, expected2) {
|
|
||||||
t.Fatalf("bad: %#v", actual2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformApply_destroyOrphan(t *testing.T) {
|
|
||||||
rpAWS := new(MockResourceProvider)
|
|
||||||
rpAWS.ResourcesReturn = []ResourceType{
|
|
||||||
ResourceType{Name: "aws_instance"},
|
|
||||||
}
|
|
||||||
rpAWS.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) {
|
|
||||||
return &ResourceDiff{
|
|
||||||
Attributes: map[string]*ResourceAttrDiff{
|
|
||||||
"num": &ResourceAttrDiff{
|
|
||||||
New: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
rpAWS.ApplyFn = func(*ResourceState, *ResourceDiff) (*ResourceState, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c := testConfig(t, "apply-error")
|
|
||||||
tf := testTerraform2(t, &Config{
|
|
||||||
Providers: map[string]ResourceProviderFactory{
|
|
||||||
"aws": testProviderFuncFixed(rpAWS),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
s := &State{
|
|
||||||
Resources: map[string]*ResourceState{
|
|
||||||
"aws_instance.baz": &ResourceState{
|
|
||||||
ID: "bar",
|
|
||||||
Type: "aws_instance",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := tf.Plan(&PlanOpts{Config: c, State: s})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := tf.Apply(p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(state.Resources) != 0 {
|
|
||||||
t.Fatalf("bad: %#v", state.Resources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformApply_error(t *testing.T) {
|
|
||||||
errored := false
|
|
||||||
|
|
||||||
rpAWS := new(MockResourceProvider)
|
|
||||||
rpAWS.ResourcesReturn = []ResourceType{
|
|
||||||
ResourceType{Name: "aws_instance"},
|
|
||||||
}
|
|
||||||
rpAWS.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) {
|
|
||||||
return &ResourceDiff{
|
|
||||||
Attributes: map[string]*ResourceAttrDiff{
|
|
||||||
"num": &ResourceAttrDiff{
|
|
||||||
New: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
rpAWS.ApplyFn = func(*ResourceState, *ResourceDiff) (*ResourceState, error) {
|
|
||||||
if errored {
|
|
||||||
return nil, fmt.Errorf("error")
|
|
||||||
}
|
|
||||||
errored = true
|
|
||||||
|
|
||||||
return &ResourceState{
|
|
||||||
ID: "foo",
|
|
||||||
Attributes: map[string]string{
|
|
||||||
"num": "2",
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c := testConfig(t, "apply-error")
|
|
||||||
tf := testTerraform2(t, &Config{
|
|
||||||
Providers: map[string]ResourceProviderFactory{
|
|
||||||
"aws": testProviderFuncFixed(rpAWS),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
p, err := tf.Plan(&PlanOpts{Config: c})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := tf.Apply(p)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("should have error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(state.Resources) != 1 {
|
|
||||||
t.Fatalf("bad: %#v", state.Resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(state.String())
|
|
||||||
expected := strings.TrimSpace(testTerraformApplyErrorStr)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad: \n%s", actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformApply_errorPartial(t *testing.T) {
|
|
||||||
errored := false
|
|
||||||
|
|
||||||
rpAWS := new(MockResourceProvider)
|
|
||||||
rpAWS.ResourcesReturn = []ResourceType{
|
|
||||||
ResourceType{Name: "aws_instance"},
|
|
||||||
}
|
|
||||||
rpAWS.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) {
|
|
||||||
return &ResourceDiff{
|
|
||||||
Attributes: map[string]*ResourceAttrDiff{
|
|
||||||
"num": &ResourceAttrDiff{
|
|
||||||
New: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
rpAWS.ApplyFn = func(*ResourceState, *ResourceDiff) (*ResourceState, error) {
|
|
||||||
if errored {
|
|
||||||
return nil, fmt.Errorf("error")
|
|
||||||
}
|
|
||||||
errored = true
|
|
||||||
|
|
||||||
return &ResourceState{
|
|
||||||
ID: "foo",
|
|
||||||
Attributes: map[string]string{
|
|
||||||
"num": "2",
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c := testConfig(t, "apply-error")
|
|
||||||
tf := testTerraform2(t, &Config{
|
|
||||||
Providers: map[string]ResourceProviderFactory{
|
|
||||||
"aws": testProviderFuncFixed(rpAWS),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
s := &State{
|
|
||||||
Resources: map[string]*ResourceState{
|
|
||||||
"aws_instance.bar": &ResourceState{
|
|
||||||
ID: "bar",
|
|
||||||
Type: "aws_instance",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := tf.Plan(&PlanOpts{Config: c, State: s})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := tf.Apply(p)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("should have error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(state.Resources) != 2 {
|
|
||||||
t.Fatalf("bad: %#v", state.Resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(state.String())
|
|
||||||
expected := strings.TrimSpace(testTerraformApplyErrorPartialStr)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad: \n%s", actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformApply_hook(t *testing.T) {
|
|
||||||
c := testConfig(t, "apply-good")
|
|
||||||
h := new(MockHook)
|
|
||||||
tf := testTerraform2(t, &Config{
|
|
||||||
Hooks: []Hook{h},
|
|
||||||
})
|
|
||||||
|
|
||||||
p, err := tf.Plan(&PlanOpts{Config: c})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := tf.Apply(p); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.PreApplyCalled {
|
|
||||||
t.Fatal("should be called")
|
|
||||||
}
|
|
||||||
if !h.PostApplyCalled {
|
|
||||||
t.Fatal("should be called")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformApply_unknownAttribute(t *testing.T) {
|
|
||||||
c := testConfig(t, "apply-unknown")
|
|
||||||
tf := testTerraform2(t, nil)
|
|
||||||
|
|
||||||
p, err := tf.Plan(&PlanOpts{Config: c})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := tf.Apply(p)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("should error")
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(state.String())
|
|
||||||
expected := strings.TrimSpace(testTerraformApplyUnknownAttrStr)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad: \n%s", actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformApply_vars(t *testing.T) {
|
|
||||||
c := testConfig(t, "apply-vars")
|
|
||||||
tf := testTerraform2(t, nil)
|
|
||||||
|
|
||||||
p, err := tf.Plan(&PlanOpts{
|
|
||||||
Config: c,
|
|
||||||
Vars: map[string]string{"foo": "baz"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicitly set the "foo" variable
|
|
||||||
p.Vars["foo"] = "bar"
|
|
||||||
|
|
||||||
state, err := tf.Apply(p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(state.String())
|
|
||||||
expected := strings.TrimSpace(testTerraformApplyVarsStr)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad: \n%s", actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformPlan(t *testing.T) {
|
|
||||||
c := testConfig(t, "plan-good")
|
|
||||||
tf := testTerraform2(t, nil)
|
|
||||||
|
|
||||||
plan, err := tf.Plan(&PlanOpts{Config: c})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(plan.Diff.Resources) < 2 {
|
|
||||||
t.Fatalf("bad: %#v", plan.Diff.Resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(plan.String())
|
|
||||||
expected := strings.TrimSpace(testTerraformPlanStr)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad:\n%s", actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformPlan_nil(t *testing.T) {
|
|
||||||
c := testConfig(t, "plan-nil")
|
|
||||||
tf := testTerraform2(t, nil)
|
|
||||||
|
|
||||||
plan, err := tf.Plan(&PlanOpts{Config: c})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
if len(plan.Diff.Resources) != 0 {
|
|
||||||
t.Fatalf("bad: %#v", plan.Diff.Resources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformPlan_computed(t *testing.T) {
|
|
||||||
c := testConfig(t, "plan-computed")
|
|
||||||
tf := testTerraform2(t, nil)
|
|
||||||
|
|
||||||
plan, err := tf.Plan(&PlanOpts{Config: c})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(plan.Diff.Resources) < 2 {
|
|
||||||
t.Fatalf("bad: %#v", plan.Diff.Resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(plan.String())
|
|
||||||
expected := strings.TrimSpace(testTerraformPlanComputedStr)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad:\n%s", actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformPlan_destroy(t *testing.T) {
|
|
||||||
c := testConfig(t, "plan-destroy")
|
|
||||||
tf := testTerraform2(t, nil)
|
|
||||||
|
|
||||||
s := &State{
|
|
||||||
Resources: map[string]*ResourceState{
|
|
||||||
"aws_instance.one": &ResourceState{
|
|
||||||
ID: "bar",
|
|
||||||
Type: "aws_instance",
|
|
||||||
},
|
|
||||||
"aws_instance.two": &ResourceState{
|
|
||||||
ID: "baz",
|
|
||||||
Type: "aws_instance",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
plan, err := tf.Plan(&PlanOpts{
|
|
||||||
Destroy: true,
|
|
||||||
Config: c,
|
|
||||||
State: s,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(plan.Diff.Resources) != 2 {
|
|
||||||
t.Fatalf("bad: %#v", plan.Diff.Resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(plan.String())
|
|
||||||
expected := strings.TrimSpace(testTerraformPlanDestroyStr)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad:\n%s", actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformPlan_hook(t *testing.T) {
|
|
||||||
c := testConfig(t, "plan-good")
|
|
||||||
h := new(MockHook)
|
|
||||||
tf := testTerraform2(t, &Config{
|
|
||||||
Hooks: []Hook{h},
|
|
||||||
})
|
|
||||||
|
|
||||||
if _, err := tf.Plan(&PlanOpts{Config: c}); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
if !h.PreDiffCalled {
|
|
||||||
t.Fatal("should be called")
|
|
||||||
}
|
|
||||||
if !h.PostDiffCalled {
|
|
||||||
t.Fatal("should be called")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformPlan_orphan(t *testing.T) {
|
|
||||||
c := testConfig(t, "plan-orphan")
|
|
||||||
tf := testTerraform2(t, nil)
|
|
||||||
|
|
||||||
s := &State{
|
|
||||||
Resources: map[string]*ResourceState{
|
|
||||||
"aws_instance.baz": &ResourceState{
|
|
||||||
ID: "bar",
|
|
||||||
Type: "aws_instance",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
plan, err := tf.Plan(&PlanOpts{
|
|
||||||
Config: c,
|
|
||||||
State: s,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(plan.String())
|
|
||||||
expected := strings.TrimSpace(testTerraformPlanOrphanStr)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad:\n%s", actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformPlan_state(t *testing.T) {
|
|
||||||
c := testConfig(t, "plan-good")
|
|
||||||
tf := testTerraform2(t, nil)
|
|
||||||
|
|
||||||
s := &State{
|
|
||||||
Resources: map[string]*ResourceState{
|
|
||||||
"aws_instance.foo": &ResourceState{
|
|
||||||
ID: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
plan, err := tf.Plan(&PlanOpts{
|
|
||||||
Config: c,
|
|
||||||
State: s,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(plan.Diff.Resources) < 2 {
|
|
||||||
t.Fatalf("bad: %#v", plan.Diff.Resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(plan.String())
|
|
||||||
expected := strings.TrimSpace(testTerraformPlanStateStr)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad:\n%s", actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformRefresh(t *testing.T) {
|
|
||||||
rpAWS := new(MockResourceProvider)
|
|
||||||
rpAWS.ResourcesReturn = []ResourceType{
|
|
||||||
ResourceType{Name: "aws_instance"},
|
|
||||||
}
|
|
||||||
|
|
||||||
c := testConfig(t, "refresh-basic")
|
|
||||||
tf := testTerraform2(t, &Config{
|
|
||||||
Providers: map[string]ResourceProviderFactory{
|
|
||||||
"aws": testProviderFuncFixed(rpAWS),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
rpAWS.RefreshReturn = &ResourceState{
|
|
||||||
ID: "foo",
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := tf.Refresh(c, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
if !rpAWS.RefreshCalled {
|
|
||||||
t.Fatal("refresh should be called")
|
|
||||||
}
|
|
||||||
if rpAWS.RefreshState.ID != "" {
|
|
||||||
t.Fatalf("bad: %#v", rpAWS.RefreshState)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(s.Resources["aws_instance.web"], rpAWS.RefreshReturn) {
|
|
||||||
t.Fatalf("bad: %#v", s.Resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range s.Resources {
|
|
||||||
if r.Type == "" {
|
|
||||||
t.Fatalf("no type: %#v", r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformRefresh_hook(t *testing.T) {
|
|
||||||
rpAWS := new(MockResourceProvider)
|
|
||||||
rpAWS.ResourcesReturn = []ResourceType{
|
|
||||||
ResourceType{Name: "aws_instance"},
|
|
||||||
}
|
|
||||||
|
|
||||||
h := new(MockHook)
|
|
||||||
|
|
||||||
c := testConfig(t, "refresh-basic")
|
|
||||||
tf := testTerraform2(t, &Config{
|
|
||||||
Hooks: []Hook{h},
|
|
||||||
Providers: map[string]ResourceProviderFactory{
|
|
||||||
"aws": testProviderFuncFixed(rpAWS),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if _, err := tf.Refresh(c, nil); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
if !h.PreRefreshCalled {
|
|
||||||
t.Fatal("should be called")
|
|
||||||
}
|
|
||||||
if h.PreRefreshState.Type != "aws_instance" {
|
|
||||||
t.Fatalf("bad: %#v", h.PreRefreshState)
|
|
||||||
}
|
|
||||||
if !h.PostRefreshCalled {
|
|
||||||
t.Fatal("should be called")
|
|
||||||
}
|
|
||||||
if h.PostRefreshState.Type != "aws_instance" {
|
|
||||||
t.Fatalf("bad: %#v", h.PostRefreshState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTerraformRefresh_state(t *testing.T) {
|
|
||||||
rpAWS := new(MockResourceProvider)
|
|
||||||
rpAWS.ResourcesReturn = []ResourceType{
|
|
||||||
ResourceType{Name: "aws_instance"},
|
|
||||||
}
|
|
||||||
|
|
||||||
c := testConfig(t, "refresh-basic")
|
|
||||||
tf := testTerraform2(t, &Config{
|
|
||||||
Providers: map[string]ResourceProviderFactory{
|
|
||||||
"aws": testProviderFuncFixed(rpAWS),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
rpAWS.RefreshReturn = &ResourceState{
|
|
||||||
ID: "foo",
|
|
||||||
}
|
|
||||||
|
|
||||||
state := &State{
|
|
||||||
Resources: map[string]*ResourceState{
|
|
||||||
"aws_instance.web": &ResourceState{
|
|
||||||
ID: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := tf.Refresh(c, state)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
if !rpAWS.RefreshCalled {
|
|
||||||
t.Fatal("refresh should be called")
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(rpAWS.RefreshState, state.Resources["aws_instance.web"]) {
|
|
||||||
t.Fatalf("bad: %#v", rpAWS.RefreshState)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(s.Resources["aws_instance.web"], rpAWS.RefreshReturn) {
|
|
||||||
t.Fatalf("bad: %#v", s.Resources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testConfig(t *testing.T, name string) *config.Config {
|
func testConfig(t *testing.T, name string) *config.Config {
|
||||||
c, err := config.Load(filepath.Join(fixtureDir, name, "main.tf"))
|
c, err := config.Load(filepath.Join(fixtureDir, name, "main.tf"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -729,171 +20,12 @@ func testConfig(t *testing.T, name string) *config.Config {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func testProviderFunc(n string, rs []string) ResourceProviderFactory {
|
|
||||||
resources := make([]ResourceType, len(rs))
|
|
||||||
for i, v := range rs {
|
|
||||||
resources[i] = ResourceType{
|
|
||||||
Name: v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return func() (ResourceProvider, error) {
|
|
||||||
p := &MockResourceProvider{Meta: n}
|
|
||||||
|
|
||||||
applyFn := func(
|
|
||||||
s *ResourceState,
|
|
||||||
d *ResourceDiff) (*ResourceState, error) {
|
|
||||||
if d.Destroy {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
id := "foo"
|
|
||||||
if idAttr, ok := d.Attributes["id"]; ok && !idAttr.NewComputed {
|
|
||||||
id = idAttr.New
|
|
||||||
}
|
|
||||||
|
|
||||||
result := &ResourceState{
|
|
||||||
ID: id,
|
|
||||||
}
|
|
||||||
|
|
||||||
if d != nil {
|
|
||||||
result = result.MergeDiff(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
if depAttr, ok := d.Attributes["dep"]; ok {
|
|
||||||
result.Dependencies = []ResourceDependency{
|
|
||||||
ResourceDependency{
|
|
||||||
ID: depAttr.New,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
diffFn := func(
|
|
||||||
s *ResourceState,
|
|
||||||
c *ResourceConfig) (*ResourceDiff, error) {
|
|
||||||
var diff ResourceDiff
|
|
||||||
diff.Attributes = make(map[string]*ResourceAttrDiff)
|
|
||||||
diff.Attributes["type"] = &ResourceAttrDiff{
|
|
||||||
Old: "",
|
|
||||||
New: s.Type,
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range c.Raw {
|
|
||||||
if _, ok := v.(string); !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if k == "nil" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This key is used for other purposes
|
|
||||||
if k == "compute_value" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if k == "compute" {
|
|
||||||
attrDiff := &ResourceAttrDiff{
|
|
||||||
Old: "",
|
|
||||||
New: "",
|
|
||||||
NewComputed: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if cv, ok := c.Config["compute_value"]; ok {
|
|
||||||
if cv.(string) == "1" {
|
|
||||||
attrDiff.NewComputed = false
|
|
||||||
attrDiff.New = fmt.Sprintf("computed_%s", v.(string))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diff.Attributes[v.(string)] = attrDiff
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this key is not computed, then look it up in the
|
|
||||||
// cleaned config.
|
|
||||||
found := false
|
|
||||||
for _, ck := range c.ComputedKeys {
|
|
||||||
if ck == k {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
v = c.Config[k]
|
|
||||||
}
|
|
||||||
|
|
||||||
attrDiff := &ResourceAttrDiff{
|
|
||||||
Old: "",
|
|
||||||
New: v.(string),
|
|
||||||
}
|
|
||||||
|
|
||||||
diff.Attributes[k] = attrDiff
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, k := range c.ComputedKeys {
|
|
||||||
diff.Attributes[k] = &ResourceAttrDiff{
|
|
||||||
Old: "",
|
|
||||||
NewComputed: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &diff, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshFn := func(s *ResourceState) (*ResourceState, error) {
|
|
||||||
if _, ok := s.Attributes["nil"]; ok {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p.ApplyFn = applyFn
|
|
||||||
p.DiffFn = diffFn
|
|
||||||
p.RefreshFn = refreshFn
|
|
||||||
p.ResourcesReturn = resources
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testProviderFuncFixed(rp ResourceProvider) ResourceProviderFactory {
|
func testProviderFuncFixed(rp ResourceProvider) ResourceProviderFactory {
|
||||||
return func() (ResourceProvider, error) {
|
return func() (ResourceProvider, error) {
|
||||||
return rp, nil
|
return rp, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testProviderMock(p ResourceProvider) *MockResourceProvider {
|
|
||||||
return p.(*MockResourceProvider)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testTerraform2(t *testing.T, c *Config) *Terraform {
|
|
||||||
if c == nil {
|
|
||||||
c = new(Config)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Providers == nil {
|
|
||||||
c.Providers = map[string]ResourceProviderFactory{
|
|
||||||
"aws": testProviderFunc("aws", []string{"aws_instance"}),
|
|
||||||
"do": testProviderFunc("do", []string{"do_droplet"}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tf, err := New(c)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
if tf == nil {
|
|
||||||
t.Fatal("tf should not be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
return tf
|
|
||||||
}
|
|
||||||
|
|
||||||
// HookRecordApplyOrder is a test hook that records the order of applies
|
// HookRecordApplyOrder is a test hook that records the order of applies
|
||||||
// by recording the PreApply event.
|
// by recording the PreApply event.
|
||||||
type HookRecordApplyOrder struct {
|
type HookRecordApplyOrder struct {
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Required
|
||||||
|
variable "foo" {
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
variable "bar" {
|
||||||
|
default = "baz"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
provider "aws" {
|
||||||
|
foo = "bar"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {}
|
|
@ -0,0 +1,3 @@
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
foo = "bar"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
resource "aws_instance" "foo" {
|
||||||
|
num = "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "bar" {
|
||||||
|
foo = "${var.foo}"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
resource "aws_instance" "foo" {
|
||||||
|
num = "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "bar" {
|
||||||
|
foo = "bar"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
variable "foo" {}
|
||||||
|
|
||||||
|
resource "aws_instance" "web" {
|
||||||
|
ami = "${var.foo}"
|
||||||
|
}
|
Loading…
Reference in New Issue