diff --git a/builtin/providers/aws/resource_provider.go b/builtin/providers/aws/resource_provider.go index 1c1b1a5a7..4b4d3b6bb 100644 --- a/builtin/providers/aws/resource_provider.go +++ b/builtin/providers/aws/resource_provider.go @@ -4,6 +4,7 @@ import ( "log" "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/multierror" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/goamz/ec2" ) @@ -18,6 +19,11 @@ func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []er 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 { if _, err := config.Decode(&p.Config, c.Config); err != nil { return err @@ -44,7 +50,7 @@ func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error { } if len(errs) > 0 { - return &terraform.MultiError{Errors: errs} + return &multierror.Error{Errors: errs} } return nil diff --git a/builtin/providers/aws/resources.go b/builtin/providers/aws/resources.go index 6aecab05c..b3a4d7769 100644 --- a/builtin/providers/aws/resources.go +++ b/builtin/providers/aws/resources.go @@ -109,18 +109,18 @@ func resource_aws_instance_diff( c *terraform.ResourceConfig, meta interface{}) (*terraform.ResourceDiff, error) { 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_ip", "private_dns", "private_ip", }, - - RequiresNewAttrs: []string{ - "ami", - "availability_zone", - "instance_type", - }, } return b.Diff(s, c) diff --git a/command/apply.go b/command/apply.go index e8e14940b..3f072e436 100644 --- a/command/apply.go +++ b/command/apply.go @@ -13,9 +13,9 @@ import ( // ApplyCommand is a Command implementation that applies a Terraform // configuration and actually builds or changes infrastructure. type ApplyCommand struct { - ShutdownCh <-chan struct{} - TFConfig *terraform.Config - Ui cli.Ui + ShutdownCh <-chan struct{} + ContextOpts *terraform.ContextOpts + Ui cli.Ui } func (c *ApplyCommand) Run(args []string) int { @@ -44,30 +44,26 @@ func (c *ApplyCommand) Run(args []string) int { 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 if init { 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 { c.Ui.Error(err.Error()) return 1 } + if !validateContext(ctx, c.Ui) { + return 1 + } errCh := make(chan error) stateCh := make(chan *terraform.State) go func() { - state, err := tf.Apply(plan) + state, err := ctx.Apply() if err != nil { errCh <- err return @@ -83,7 +79,7 @@ func (c *ApplyCommand) Run(args []string) int { c.Ui.Output("Interrupt received. Gracefully shutting down...") // Stop execution - tf.Stop() + ctx.Stop() // Still get the result, since there is still one select { diff --git a/command/apply_test.go b/command/apply_test.go index 820b510b4..8f89f8221 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -16,8 +17,8 @@ func TestApply(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ - TFConfig: testTFConfig(p), - Ui: ui, + ContextOpts: testCtxConfig(p), + Ui: ui, } 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) { - planPath := testPlanFile(t, new(terraform.Plan)) + planPath := testPlanFile(t, &terraform.Plan{ + Config: new(config.Config), + }) statePath := testTempFile(t) p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ - TFConfig: testTFConfig(p), - Ui: ui, + ContextOpts: testCtxConfig(p), + Ui: ui, } args := []string{ @@ -97,9 +118,9 @@ func TestApply_shutdown(t *testing.T) { shutdownCh := make(chan struct{}) ui := new(cli.MockUi) c := &ApplyCommand{ - ShutdownCh: shutdownCh, - TFConfig: testTFConfig(p), - Ui: ui, + ContextOpts: testCtxConfig(p), + ShutdownCh: shutdownCh, + Ui: ui, } p.DiffFn = func( @@ -197,8 +218,8 @@ func TestApply_state(t *testing.T) { ui := new(cli.MockUi) c := &ApplyCommand{ - TFConfig: testTFConfig(p), - Ui: ui, + ContextOpts: testCtxConfig(p), + Ui: ui, } // Run the apply command pointing to our existing state @@ -244,8 +265,8 @@ func TestApply_stateNoExist(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ - TFConfig: testTFConfig(p), - Ui: ui, + ContextOpts: testCtxConfig(p), + Ui: ui, } args := []string{ diff --git a/command/command.go b/command/command.go index d42584d23..fd316f4b1 100644 --- a/command/command.go +++ b/command/command.go @@ -6,19 +6,20 @@ import ( "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" ) -func PlanArg( +func ContextArg( path 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. f, err := os.Open(path) if err == nil { plan, err := terraform.ReadPlan(f) f.Close() if err == nil { - return plan, nil + return plan.Context(opts), nil } } @@ -55,14 +56,47 @@ func PlanArg( if err != nil { 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{ - Config: config, - State: state, - }) - if err != nil { + opts.Config = config + opts.State = state + ctx := terraform.NewContext(opts) + + if _, err := ctx.Plan(nil); err != nil { 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 } diff --git a/command/command_test.go b/command/command_test.go index 074db94b8..2f3207aa7 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -16,8 +16,8 @@ func testFixturePath(name string) string { return filepath.Join(fixtureDir, name, "main.tf") } -func testTFConfig(p terraform.ResourceProvider) *terraform.Config { - return &terraform.Config{ +func testCtxConfig(p terraform.ResourceProvider) *terraform.ContextOpts { + return &terraform.ContextOpts{ Providers: map[string]terraform.ResourceProviderFactory{ "test": func() (terraform.ResourceProvider, error) { return p, nil diff --git a/command/config.go b/command/config.go new file mode 100644 index 000000000..c01016df7 --- /dev/null +++ b/command/config.go @@ -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, + } +} diff --git a/command/graph.go b/command/graph.go index bca522e9d..0d36166cd 100644 --- a/command/graph.go +++ b/command/graph.go @@ -15,8 +15,8 @@ import ( // GraphCommand is a Command implementation that takes a Terraform // configuration and outputs the dependency tree in graphical form. type GraphCommand struct { - TFConfig *terraform.Config - Ui cli.Ui + ContextOpts *terraform.ContextOpts + Ui cli.Ui } func (c *GraphCommand) Run(args []string) int { @@ -41,7 +41,7 @@ func (c *GraphCommand) Run(args []string) int { g, err := terraform.Graph(&terraform.GraphOpts{ Config: conf, - Providers: c.TFConfig.Providers, + Providers: c.ContextOpts.Providers, }) if err != nil { c.Ui.Error(fmt.Sprintf("Error creating graph: %s", err)) diff --git a/command/plan.go b/command/plan.go index 5781f0156..06cb9e7bb 100644 --- a/command/plan.go +++ b/command/plan.go @@ -15,8 +15,8 @@ import ( // PlanCommand is a Command implementation that compares a Terraform // configuration to an actual infrastructure and shows the differences. type PlanCommand struct { - TFConfig *terraform.Config - Ui cli.Ui + ContextOpts *terraform.ContextOpts + Ui cli.Ui } func (c *PlanCommand) Run(args []string) int { @@ -65,26 +65,22 @@ func (c *PlanCommand) Run(args []string) int { return 1 } - 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)) + c.ContextOpts.Config = b + c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui}) + c.ContextOpts.State = state + ctx := terraform.NewContext(c.ContextOpts) + if !validateContext(ctx, c.Ui) { return 1 } if refresh { - state, err = tf.Refresh(b, state) - if err != nil { + if _, err := ctx.Refresh(); err != nil { c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) return 1 } } - plan, err := tf.Plan(&terraform.PlanOpts{ - Config: b, - Destroy: destroy, - State: state, - }) + plan, err := ctx.Plan(&terraform.PlanOpts{Destroy: destroy}) if err != nil { c.Ui.Error(fmt.Sprintf("Error running plan: %s", err)) return 1 diff --git a/command/plan_test.go b/command/plan_test.go index 4ef71cb2e..948cd73d9 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -26,8 +26,8 @@ func TestPlan_destroy(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &PlanCommand{ - TFConfig: testTFConfig(p), - Ui: ui, + ContextOpts: testCtxConfig(p), + Ui: ui, } args := []string{ @@ -51,8 +51,8 @@ func TestPlan_noState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &PlanCommand{ - TFConfig: testTFConfig(p), - Ui: ui, + ContextOpts: testCtxConfig(p), + Ui: ui, } args := []string{ @@ -87,8 +87,8 @@ func TestPlan_outPath(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &PlanCommand{ - TFConfig: testTFConfig(p), - Ui: ui, + ContextOpts: testCtxConfig(p), + Ui: ui, } p.DiffReturn = &terraform.ResourceDiff{ @@ -118,8 +118,8 @@ func TestPlan_refresh(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &PlanCommand{ - TFConfig: testTFConfig(p), - Ui: ui, + ContextOpts: testCtxConfig(p), + Ui: ui, } args := []string{ @@ -162,8 +162,8 @@ func TestPlan_state(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &PlanCommand{ - TFConfig: testTFConfig(p), - Ui: ui, + ContextOpts: testCtxConfig(p), + Ui: ui, } args := []string{ diff --git a/command/refresh.go b/command/refresh.go index b3c9f4bb1..f1447b781 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -15,8 +15,8 @@ import ( // RefreshCommand is a cli.Command implementation that refreshes the state // file. type RefreshCommand struct { - TFConfig *terraform.Config - Ui cli.Ui + ContextOpts *terraform.ContextOpts + Ui cli.Ui } func (c *RefreshCommand) Run(args []string) int { @@ -66,14 +66,14 @@ func (c *RefreshCommand) Run(args []string) int { return 1 } - 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)) + c.ContextOpts.Config = b + c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui}) + ctx := terraform.NewContext(c.ContextOpts) + if !validateContext(ctx, c.Ui) { return 1 } - state, err = tf.Refresh(b, state) + state, err = ctx.Refresh() if err != nil { c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) return 1 diff --git a/command/refresh_test.go b/command/refresh_test.go index 192f42137..9f65e92c1 100644 --- a/command/refresh_test.go +++ b/command/refresh_test.go @@ -30,8 +30,8 @@ func TestRefresh(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &RefreshCommand{ - TFConfig: testTFConfig(p), - Ui: ui, + ContextOpts: testCtxConfig(p), + Ui: ui, } p.RefreshFn = nil @@ -96,8 +96,8 @@ func TestRefresh_outPath(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &RefreshCommand{ - TFConfig: testTFConfig(p), - Ui: ui, + ContextOpts: testCtxConfig(p), + Ui: ui, } p.RefreshFn = nil diff --git a/command/test-fixtures/apply-config-invalid/main.tf b/command/test-fixtures/apply-config-invalid/main.tf new file mode 100644 index 000000000..ea4d9df6e --- /dev/null +++ b/command/test-fixtures/apply-config-invalid/main.tf @@ -0,0 +1,3 @@ +resource "test_instance" "foo" { + ami = "${var.nope}" +} diff --git a/commands.go b/commands.go index 2ff36f53d..48d57cbfe 100644 --- a/commands.go +++ b/commands.go @@ -29,30 +29,30 @@ func init() { Commands = map[string]cli.CommandFactory{ "apply": func() (cli.Command, error) { return &command.ApplyCommand{ - ShutdownCh: makeShutdownCh(), - TFConfig: &TFConfig, - Ui: Ui, + ShutdownCh: makeShutdownCh(), + ContextOpts: &ContextOpts, + Ui: Ui, }, nil }, "graph": func() (cli.Command, error) { return &command.GraphCommand{ - TFConfig: &TFConfig, - Ui: Ui, + ContextOpts: &ContextOpts, + Ui: Ui, }, nil }, "plan": func() (cli.Command, error) { return &command.PlanCommand{ - TFConfig: &TFConfig, - Ui: Ui, + ContextOpts: &ContextOpts, + Ui: Ui, }, nil }, "refresh": func() (cli.Command, error) { return &command.RefreshCommand{ - TFConfig: &TFConfig, - Ui: Ui, + ContextOpts: &ContextOpts, + Ui: Ui, }, nil }, diff --git a/config.go b/config.go index d0085ab91..c3cfb0f9f 100644 --- a/config.go +++ b/config.go @@ -11,12 +11,6 @@ import ( "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. // // 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. 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 // equally behaving parsing everywhere. const libuclParseFlags = libucl.ParserKeyLowercase diff --git a/config/config.go b/config/config.go index fde51d178..8c8b80fa5 100644 --- a/config/config.go +++ b/config/config.go @@ -5,6 +5,8 @@ package config import ( "fmt" "strings" + + "github.com/hashicorp/terraform/helper/multierror" ) // 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. func (c *Config) Validate() error { - // TODO(mitchellh): make sure all referenced variables exist - // TODO(mitchellh): make sure types/names have valid values (characters) + var errs []error + + 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 } +// 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. func (v *Variable) Required() bool { return !v.defaultSet diff --git a/config/config_test.go b/config/config_test.go index 22c4c4d33..4974b7276 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,12 +1,34 @@ package config import ( + "path/filepath" "testing" ) // This is the directory where our test fixtures are. 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) { v, err := NewResourceVariable("foo.bar.baz") if err != nil { @@ -55,3 +77,12 @@ func TestProviderConfigName(t *testing.T) { 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 +} diff --git a/config/test-fixtures/validate-good/main.tf b/config/test-fixtures/validate-good/main.tf new file mode 100644 index 000000000..7cd229611 --- /dev/null +++ b/config/test-fixtures/validate-good/main.tf @@ -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" + } +} diff --git a/config/test-fixtures/validate-unknown-resource-var/main.tf b/config/test-fixtures/validate-unknown-resource-var/main.tf new file mode 100644 index 000000000..3c89cc013 --- /dev/null +++ b/config/test-fixtures/validate-unknown-resource-var/main.tf @@ -0,0 +1,6 @@ +resource "aws_instance" "web" { +} + +resource "aws_instance" "db" { + ami = "${aws_instance.loadbalancer.foo}" +} diff --git a/config/test-fixtures/validate-unknownvar/main.tf b/config/test-fixtures/validate-unknownvar/main.tf new file mode 100644 index 000000000..7d1a77bfb --- /dev/null +++ b/config/test-fixtures/validate-unknownvar/main.tf @@ -0,0 +1,8 @@ +variable "foo" { + default = "bar"; + description = "bar"; +} + +provider "do" { + api_key = "${var.bar}"; +} diff --git a/terraform/multi_error.go b/helper/multierror/error.go similarity index 54% rename from terraform/multi_error.go rename to helper/multierror/error.go index 9f11b95d8..cddd2fc84 100644 --- a/terraform/multi_error.go +++ b/helper/multierror/error.go @@ -1,18 +1,18 @@ -package terraform +package multierror import ( "fmt" "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 // them as a single error. -type MultiError struct { +type Error struct { Errors []error } -func (e *MultiError) Error() string { +func (e *Error) Error() string { points := make([]string, len(e.Errors)) for i, err := range e.Errors { points[i] = fmt.Sprintf("* %s", err) @@ -23,18 +23,18 @@ func (e *MultiError) Error() string { len(e.Errors), strings.Join(points, "\n")) } -// MultiErrorAppend is a helper function that will append more errors -// onto a MultiError in order to create a larger multi-error. If the -// original error is not a MultiError, it will be turned into one. -func MultiErrorAppend(err error, errs ...error) *MultiError { +// ErrorAppend is a helper function that will append more errors +// onto a Error in order to create a larger multi-error. If the +// original error is not a Error, it will be turned into one. +func ErrorAppend(err error, errs ...error) *Error { if err == nil { - err = new(MultiError) + err = new(Error) } switch err := err.(type) { - case *MultiError: + case *Error: if err == nil { - err = new(MultiError) + err = new(Error) } err.Errors = append(err.Errors, errs...) @@ -43,7 +43,7 @@ func MultiErrorAppend(err error, errs ...error) *MultiError { newErrs := make([]error, len(errs)+1) newErrs[0] = err copy(newErrs[1:], errs) - return &MultiError{ + return &Error{ Errors: newErrs, } } diff --git a/terraform/multi_error_test.go b/helper/multierror/error_test.go similarity index 53% rename from terraform/multi_error_test.go rename to helper/multierror/error_test.go index 44e07babf..207c00465 100644 --- a/terraform/multi_error_test.go +++ b/helper/multierror/error_test.go @@ -1,19 +1,19 @@ -package terraform +package multierror import ( "errors" "testing" ) -func TestMultiError_Impl(t *testing.T) { +func TestError_Impl(t *testing.T) { var raw interface{} - raw = &MultiError{} + raw = &Error{} 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: * foo @@ -24,32 +24,32 @@ func TestMultiErrorError(t *testing.T) { errors.New("bar"), } - multi := &MultiError{errors} + multi := &Error{errors} if multi.Error() != expected { t.Fatalf("bad: %s", multi.Error()) } } -func TestMultiErrorAppend_MultiError(t *testing.T) { - original := &MultiError{ +func TestErrorAppend_Error(t *testing.T) { + original := &Error{ Errors: []error{errors.New("foo")}, } - result := MultiErrorAppend(original, errors.New("bar")) + result := ErrorAppend(original, errors.New("bar")) if len(result.Errors) != 2 { t.Fatalf("wrong len: %d", len(result.Errors)) } - original = &MultiError{} - result = MultiErrorAppend(original, errors.New("bar")) + original = &Error{} + result = ErrorAppend(original, errors.New("bar")) if len(result.Errors) != 1 { t.Fatalf("wrong len: %d", len(result.Errors)) } } -func TestMultiErrorAppend_NonMultiError(t *testing.T) { +func TestErrorAppend_NonError(t *testing.T) { original := errors.New("foo") - result := MultiErrorAppend(original, errors.New("bar")) + result := ErrorAppend(original, errors.New("bar")) if len(result.Errors) != 2 { t.Fatalf("wrong len: %d", len(result.Errors)) } diff --git a/main.go b/main.go index 31c0d5aed..c47ecca55 100644 --- a/main.go +++ b/main.go @@ -84,7 +84,7 @@ func wrappedMain() int { defer plugin.CleanupClients() // 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 // just show the version. diff --git a/rpc/resource_provider.go b/rpc/resource_provider.go index d74ffe165..3bbca0d9a 100644 --- a/rpc/resource_provider.go +++ b/rpc/resource_provider.go @@ -35,6 +35,30 @@ func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []er 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 { var resp ResourceProviderConfigureResponse err := p.Client.Call(p.Name+".Configure", c, &resp) @@ -157,6 +181,16 @@ type ResourceProviderValidateResponse struct { Errors []*BasicError } +type ResourceProviderValidateResourceArgs struct { + Config *terraform.ResourceConfig + Type string +} + +type ResourceProviderValidateResourceResponse struct { + Warnings []string + Errors []*BasicError +} + func (s *ResourceProviderServer) Validate( args *ResourceProviderValidateArgs, reply *ResourceProviderValidateResponse) error { @@ -172,6 +206,21 @@ func (s *ResourceProviderServer) Validate( 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( config *terraform.ResourceConfig, reply *ResourceProviderConfigureResponse) error { diff --git a/rpc/resource_provider_test.go b/rpc/resource_provider_test.go index 8b94712fc..72e3340e6 100644 --- a/rpc/resource_provider_test.go +++ b/rpc/resource_provider_test.go @@ -341,3 +341,106 @@ func TestResourceProvider_validate_warns(t *testing.T) { 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) + } +} diff --git a/terraform/context.go b/terraform/context.go new file mode 100644 index 000000000..ebf4d7bd3 --- /dev/null +++ b/terraform/context.go @@ -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 + } +} diff --git a/terraform/context_test.go b/terraform/context_test.go new file mode 100644 index 000000000..3f51483de --- /dev/null +++ b/terraform/context_test.go @@ -0,0 +1,1000 @@ +package terraform + +import ( + "fmt" + "reflect" + "strings" + "testing" +) + +func TestContextValidate(t *testing.T) { + config := testConfig(t, "validate-good") + c := testContext(t, &ContextOpts{ + Config: config, + }) + + w, e := c.Validate() + if len(w) > 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) > 0 { + t.Fatalf("bad: %#v", e) + } +} + +func TestContextValidate_badVar(t *testing.T) { + config := testConfig(t, "validate-bad-var") + c := testContext(t, &ContextOpts{ + Config: config, + }) + + w, e := c.Validate() + if len(w) > 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) == 0 { + t.Fatalf("bad: %#v", e) + } +} + +func TestContextValidate_providerConfig_bad(t *testing.T) { + config := testConfig(t, "validate-bad-pc") + p := testProvider("aws") + c := testContext(t, &ContextOpts{ + Config: config, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + p.ValidateReturnErrors = []error{fmt.Errorf("bad")} + + w, e := c.Validate() + if len(w) > 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) == 0 { + t.Fatalf("bad: %#v", e) + } +} + +func TestContextValidate_providerConfig_good(t *testing.T) { + config := testConfig(t, "validate-bad-pc") + p := testProvider("aws") + c := testContext(t, &ContextOpts{ + Config: config, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + w, e := c.Validate() + if len(w) > 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) > 0 { + t.Fatalf("bad: %#v", e) + } +} + +func TestContextValidate_resourceConfig_bad(t *testing.T) { + config := testConfig(t, "validate-bad-rc") + p := testProvider("aws") + c := testContext(t, &ContextOpts{ + Config: config, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + p.ValidateResourceReturnErrors = []error{fmt.Errorf("bad")} + + w, e := c.Validate() + if len(w) > 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) == 0 { + t.Fatalf("bad: %#v", e) + } +} + +func TestContextValidate_resourceConfig_good(t *testing.T) { + config := testConfig(t, "validate-bad-rc") + p := testProvider("aws") + c := testContext(t, &ContextOpts{ + Config: config, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + w, e := c.Validate() + if len(w) > 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) > 0 { + t.Fatalf("bad: %#v", e) + } +} + +func TestContextValidate_requiredVar(t *testing.T) { + config := testConfig(t, "validate-required-var") + c := testContext(t, &ContextOpts{ + Config: config, + }) + + w, e := c.Validate() + if len(w) > 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) == 0 { + t.Fatalf("bad: %#v", e) + } +} + +func TestContextApply(t *testing.T) { + c := testConfig(t, "apply-good") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + 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 TestContextApply_cancel(t *testing.T) { + stopped := false + + c := testConfig(t, "apply-cancel") + p := testProvider("aws") + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + p.ApplyFn = func(*ResourceState, *ResourceDiff) (*ResourceState, error) { + if !stopped { + stopped = true + go ctx.Stop() + + for { + if ctx.sh.Stopped() { + break + } + } + } + + return &ResourceState{ + ID: "foo", + Attributes: map[string]string{ + "num": "2", + }, + }, nil + } + p.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) { + return &ResourceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "num": &ResourceAttrDiff{ + New: "bar", + }, + }, + }, nil + } + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + // Start the Apply in a goroutine + stateCh := make(chan *State) + go func() { + state, err := ctx.Apply() + if err != nil { + panic(err) + } + + stateCh <- state + }() + + 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 TestContextApply_compute(t *testing.T) { + c := testConfig(t, "apply-compute") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + ctx.variables = map[string]string{"value": "1"} + + state, err := ctx.Apply() + 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 TestContextApply_destroy(t *testing.T) { + c := testConfig(t, "apply-destroy") + h := new(HookRecordApplyOrder) + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + // First plan and apply a create operation + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + if _, err := ctx.Apply(); err != nil { + t.Fatalf("err: %s", err) + } + + // Next, plan and apply a destroy operation + if _, err := ctx.Plan(&PlanOpts{Destroy: true}); err != nil { + t.Fatalf("err: %s", err) + } + + h.Active = true + + state, err := ctx.Apply() + 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 TestContextApply_destroyOrphan(t *testing.T) { + c := testConfig(t, "apply-error") + p := testProvider("aws") + s := &State{ + Resources: map[string]*ResourceState{ + "aws_instance.baz": &ResourceState{ + ID: "bar", + Type: "aws_instance", + }, + }, + } + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + p.ApplyFn = func(*ResourceState, *ResourceDiff) (*ResourceState, error) { + return nil, nil + } + p.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) { + return &ResourceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "num": &ResourceAttrDiff{ + New: "bar", + }, + }, + }, nil + } + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + if len(state.Resources) != 0 { + t.Fatalf("bad: %#v", state.Resources) + } +} + +func TestContextApply_error(t *testing.T) { + errored := false + + c := testConfig(t, "apply-error") + p := testProvider("aws") + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + p.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 + } + p.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) { + return &ResourceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "num": &ResourceAttrDiff{ + New: "bar", + }, + }, + }, nil + } + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + 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 TestContextApply_errorPartial(t *testing.T) { + errored := false + + c := testConfig(t, "apply-error") + p := testProvider("aws") + s := &State{ + Resources: map[string]*ResourceState{ + "aws_instance.bar": &ResourceState{ + ID: "bar", + Type: "aws_instance", + }, + }, + } + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + p.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 + } + p.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) { + return &ResourceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "num": &ResourceAttrDiff{ + New: "bar", + }, + }, + }, nil + } + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + 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 TestContextApply_hook(t *testing.T) { + c := testConfig(t, "apply-good") + h := new(MockHook) + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + if _, err := ctx.Apply(); err != nil { + t.Fatalf("err: %s", err) + } + + if !h.PreApplyCalled { + t.Fatal("should be called") + } + if !h.PostApplyCalled { + t.Fatal("should be called") + } +} + +func TestContextApply_unknownAttribute(t *testing.T) { + c := testConfig(t, "apply-unknown") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + 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 TestContextApply_vars(t *testing.T) { + c := testConfig(t, "apply-vars") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Variables: map[string]string{ + "foo": "bar", + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + 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 TestContextPlan(t *testing.T) { + c := testConfig(t, "plan-good") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + 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 TestContextPlan_nil(t *testing.T) { + c := testConfig(t, "plan-nil") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if len(plan.Diff.Resources) != 0 { + t.Fatalf("bad: %#v", plan.Diff.Resources) + } +} + +func TestContextPlan_computed(t *testing.T) { + c := testConfig(t, "plan-computed") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + 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 TestContextPlan_destroy(t *testing.T) { + c := testConfig(t, "plan-destroy") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Resources: map[string]*ResourceState{ + "aws_instance.one": &ResourceState{ + ID: "bar", + Type: "aws_instance", + }, + "aws_instance.two": &ResourceState{ + ID: "baz", + Type: "aws_instance", + }, + }, + } + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(&PlanOpts{Destroy: true}) + 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 TestContextPlan_hook(t *testing.T) { + c := testConfig(t, "plan-good") + h := new(MockHook) + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + _, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + if !h.PreDiffCalled { + t.Fatal("should be called") + } + if !h.PostDiffCalled { + t.Fatal("should be called") + } +} + +func TestContextPlan_orphan(t *testing.T) { + c := testConfig(t, "plan-orphan") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Resources: map[string]*ResourceState{ + "aws_instance.baz": &ResourceState{ + ID: "bar", + Type: "aws_instance", + }, + }, + } + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(nil) + 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 TestContextPlan_state(t *testing.T) { + c := testConfig(t, "plan-good") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + ID: "bar", + }, + }, + } + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(nil) + 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 TestContextRefresh(t *testing.T) { + p := testProvider("aws") + c := testConfig(t, "refresh-basic") + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + p.RefreshFn = nil + p.RefreshReturn = &ResourceState{ + ID: "foo", + } + + s, err := ctx.Refresh() + if err != nil { + t.Fatalf("err: %s", err) + } + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } + if p.RefreshState.ID != "" { + t.Fatalf("bad: %#v", p.RefreshState) + } + if !reflect.DeepEqual(s.Resources["aws_instance.web"], p.RefreshReturn) { + t.Fatalf("bad: %#v", s.Resources["aws_instance.web"]) + } + + for _, r := range s.Resources { + if r.Type == "" { + t.Fatalf("no type: %#v", r) + } + } +} + +func TestContextRefresh_hook(t *testing.T) { + h := new(MockHook) + p := testProvider("aws") + c := testConfig(t, "refresh-basic") + ctx := testContext(t, &ContextOpts{ + Config: c, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Refresh(); 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 TestContextRefresh_state(t *testing.T) { + p := testProvider("aws") + c := testConfig(t, "refresh-basic") + state := &State{ + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + ID: "bar", + }, + }, + } + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: state, + }) + + p.RefreshFn = nil + p.RefreshReturn = &ResourceState{ + ID: "foo", + } + + s, err := ctx.Refresh() + if err != nil { + t.Fatalf("err: %s", err) + } + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } + if !reflect.DeepEqual(p.RefreshState, state.Resources["aws_instance.web"]) { + t.Fatalf("bad: %#v", p.RefreshState) + } + if !reflect.DeepEqual(s.Resources["aws_instance.web"], p.RefreshReturn) { + t.Fatalf("bad: %#v", s.Resources) + } +} + +func testContext(t *testing.T, opts *ContextOpts) *Context { + return NewContext(opts) +} + +func testApplyFn( + 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 +} + +func testDiffFn( + 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 +} + +func testProvider(prefix string) *MockResourceProvider { + p := new(MockResourceProvider) + p.RefreshFn = func(s *ResourceState) (*ResourceState, error) { + return s, nil + } + p.ResourcesReturn = []ResourceType{ + ResourceType{ + Name: fmt.Sprintf("%s_instance", prefix), + }, + } + + return p +} diff --git a/terraform/graph.go b/terraform/graph.go index 4380ce751..486996cc4 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/depgraph" + "github.com/hashicorp/terraform/helper/multierror" ) // GraphOpts are options used to create the resource graph that Terraform @@ -325,7 +326,7 @@ func graphAddMissingResourceProviders( } if len(errs) > 0 { - return &MultiError{Errors: errs} + return &multierror.Error{Errors: errs} } return nil @@ -518,7 +519,7 @@ func graphInitResourceProviders( } if len(errs) > 0 { - return &MultiError{Errors: errs} + return &multierror.Error{Errors: errs} } return nil @@ -583,7 +584,7 @@ func graphMapResourceProviders(g *depgraph.Graph) error { } if len(errs) > 0 { - return &MultiError{Errors: errs} + return &multierror.Error{Errors: errs} } return nil diff --git a/terraform/hook_stop.go b/terraform/hook_stop.go index 9ed8aacad..b0c19aaaf 100644 --- a/terraform/hook_stop.go +++ b/terraform/hook_stop.go @@ -1,24 +1,13 @@ package terraform import ( - "sync" + "sync/atomic" ) // stopHook is a private Hook implementation that Terraform uses to // signal when to stop or cancel actions. type stopHook struct { - sync.Mutex - - // 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{} + stop uint32 } 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) { - select { - case <-h.ch: - h.stoppedCh <- struct{}{} + if h.Stopped() { return HookActionHalt, nil - default: - return HookActionContinue, nil } + + return HookActionContinue, nil } // reset should be called within the lock context -func (h *stopHook) reset() { - h.ch = make(chan struct{}) - h.count = 0 - h.serial += 1 - h.stoppedCh = nil +func (h *stopHook) Reset() { + atomic.StoreUint32(&h.stop, 0) } -func (h *stopHook) ref() int { - h.Lock() - defer h.Unlock() - h.count++ - return h.serial +func (h *stopHook) Stop() { + atomic.StoreUint32(&h.stop, 1) } -func (h *stopHook) unref(s int) { - h.Lock() - defer h.Unlock() - if h.serial == s { - h.count-- - } +func (h *stopHook) Stopped() bool { + return atomic.LoadUint32(&h.stop) == 1 } diff --git a/terraform/plan.go b/terraform/plan.go index 8b6f37a9d..081fc6985 100644 --- a/terraform/plan.go +++ b/terraform/plan.go @@ -23,10 +23,6 @@ type PlanOpts struct { // that are created. Otherwise, it will move towards the desired state // specified in the configuration. Destroy bool - - Config *config.Config - State *State - Vars map[string]string } // Plan represents a single Terraform execution plan, which contains @@ -40,6 +36,18 @@ type Plan struct { 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 { buf := new(bytes.Buffer) buf.WriteString("DIFF:\n\n") diff --git a/terraform/resource_provider.go b/terraform/resource_provider.go index 75410c2ea..554d31c4f 100644 --- a/terraform/resource_provider.go +++ b/terraform/resource_provider.go @@ -17,14 +17,31 @@ type ResourceProvider interface { // (no interpolation done) and can return a list of warnings and/or // errors. // + // This is called once with the provider configuration only. It may not + // be called at all if no provider configuration is given. + // // This should not assume that any values of the configurations are valid. // The primary use case of this call is to check that required keys are // set. Validate(*ResourceConfig) ([]string, []error) + // 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 // given. This is useful for setting things like access keys. // + // This won't be called at all if no provider configuration is given. + // // Configure returns an error if it occurred. Configure(*ResourceConfig) error diff --git a/terraform/resource_provider_mock.go b/terraform/resource_provider_mock.go index 9532b4efb..595e20f47 100644 --- a/terraform/resource_provider_mock.go +++ b/terraform/resource_provider_mock.go @@ -6,32 +6,37 @@ type MockResourceProvider struct { // Anything you want, in case you need to store extra data with the mock. Meta interface{} - ApplyCalled bool - ApplyState *ResourceState - ApplyDiff *ResourceDiff - ApplyFn func(*ResourceState, *ResourceDiff) (*ResourceState, error) - ApplyReturn *ResourceState - ApplyReturnError error - ConfigureCalled bool - ConfigureConfig *ResourceConfig - ConfigureReturnError error - DiffCalled bool - DiffState *ResourceState - DiffDesired *ResourceConfig - DiffFn func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) - DiffReturn *ResourceDiff - DiffReturnError error - RefreshCalled bool - RefreshState *ResourceState - RefreshFn func(*ResourceState) (*ResourceState, error) - RefreshReturn *ResourceState - RefreshReturnError error - ResourcesCalled bool - ResourcesReturn []ResourceType - ValidateCalled bool - ValidateConfig *ResourceConfig - ValidateReturnWarns []string - ValidateReturnErrors []error + ApplyCalled bool + ApplyState *ResourceState + ApplyDiff *ResourceDiff + ApplyFn func(*ResourceState, *ResourceDiff) (*ResourceState, error) + ApplyReturn *ResourceState + ApplyReturnError error + ConfigureCalled bool + ConfigureConfig *ResourceConfig + ConfigureReturnError error + DiffCalled bool + DiffState *ResourceState + DiffDesired *ResourceConfig + DiffFn func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) + DiffReturn *ResourceDiff + DiffReturnError error + RefreshCalled bool + RefreshState *ResourceState + RefreshFn func(*ResourceState) (*ResourceState, error) + RefreshReturn *ResourceState + RefreshReturnError error + ResourcesCalled bool + ResourcesReturn []ResourceType + ValidateCalled bool + ValidateConfig *ResourceConfig + ValidateReturnWarns []string + ValidateReturnErrors []error + ValidateResourceCalled bool + ValidateResourceType string + ValidateResourceConfig *ResourceConfig + ValidateResourceReturnWarns []string + ValidateResourceReturnErrors []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 } +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 { p.ConfigureCalled = true p.ConfigureConfig = c diff --git a/terraform/semantics.go b/terraform/semantics.go index adafdbaa5..0a40506fd 100644 --- a/terraform/semantics.go +++ b/terraform/semantics.go @@ -1,147 +1,24 @@ package terraform -/* import ( "fmt" "github.com/hashicorp/terraform/config" ) -/* -// smcProviders matches up the resources with a provider and initializes -// it. This does not call "Configure" on the ResourceProvider, since that -// 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 { +// smcUserVariables does all the semantic checks to verify that the +// variables given satisfy the configuration itself. +func smcUserVariables(c *config.Config, vs map[string]string) []error { var errs []error // Check that all required variables are present required := make(map[string]struct{}) - for k, v := range c.Config.Variables { + for k, v := range c.Variables { if v.Required() { required[k] = struct{}{} } } - for k, _ := range c.Variables { + for k, _ := range vs { delete(required, k) } if len(required) > 0 { @@ -155,4 +32,3 @@ func smcVariables(c *Config) []error { return errs } -*/ diff --git a/terraform/semantics_test.go b/terraform/semantics_test.go new file mode 100644 index 000000000..9d7d30ebc --- /dev/null +++ b/terraform/semantics_test.go @@ -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) + } +} diff --git a/terraform/terraform.go b/terraform/terraform.go deleted file mode 100644 index 5b4fe60c5..000000000 --- a/terraform/terraform.go +++ /dev/null @@ -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 - } -} diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 1f9b1d83d..04d79de8d 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -1,10 +1,7 @@ package terraform import ( - "fmt" "path/filepath" - "reflect" - "strings" "sync" "testing" @@ -14,712 +11,6 @@ import ( // This is the directory where our test fixtures are. 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 { c, err := config.Load(filepath.Join(fixtureDir, name, "main.tf")) if err != nil { @@ -729,171 +20,12 @@ func testConfig(t *testing.T, name string) *config.Config { 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 { return func() (ResourceProvider, error) { 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 // by recording the PreApply event. type HookRecordApplyOrder struct { diff --git a/terraform/test-fixtures/smc-uservars/main.tf b/terraform/test-fixtures/smc-uservars/main.tf new file mode 100644 index 000000000..7e2d10c11 --- /dev/null +++ b/terraform/test-fixtures/smc-uservars/main.tf @@ -0,0 +1,8 @@ +# Required +variable "foo" { +} + +# Optional +variable "bar" { + default = "baz" +} diff --git a/terraform/test-fixtures/validate-bad-pc/main.tf b/terraform/test-fixtures/validate-bad-pc/main.tf new file mode 100644 index 000000000..2aada399b --- /dev/null +++ b/terraform/test-fixtures/validate-bad-pc/main.tf @@ -0,0 +1,5 @@ +provider "aws" { + foo = "bar" +} + +resource "aws_instance" "test" {} diff --git a/terraform/test-fixtures/validate-bad-rc/main.tf b/terraform/test-fixtures/validate-bad-rc/main.tf new file mode 100644 index 000000000..292a1443e --- /dev/null +++ b/terraform/test-fixtures/validate-bad-rc/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "test" { + foo = "bar" +} diff --git a/terraform/test-fixtures/validate-bad-var/main.tf b/terraform/test-fixtures/validate-bad-var/main.tf new file mode 100644 index 000000000..f5c9c684f --- /dev/null +++ b/terraform/test-fixtures/validate-bad-var/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "foo" { + num = "2" +} + +resource "aws_instance" "bar" { + foo = "${var.foo}" +} diff --git a/terraform/test-fixtures/validate-good/main.tf b/terraform/test-fixtures/validate-good/main.tf new file mode 100644 index 000000000..ce5654441 --- /dev/null +++ b/terraform/test-fixtures/validate-good/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "foo" { + num = "2" +} + +resource "aws_instance" "bar" { + foo = "bar" +} diff --git a/terraform/test-fixtures/validate-required-var/main.tf b/terraform/test-fixtures/validate-required-var/main.tf new file mode 100644 index 000000000..a7907b654 --- /dev/null +++ b/terraform/test-fixtures/validate-required-var/main.tf @@ -0,0 +1,5 @@ +variable "foo" {} + +resource "aws_instance" "web" { + ami = "${var.foo}" +}