Merge pull request #12 from hashicorp/f-semantics

Richer Validation, Semantic Checks, Core Refactor
This commit is contained in:
Mitchell Hashimoto 2014-07-03 13:15:01 -07:00
commit 3decae513e
42 changed files with 2245 additions and 1646 deletions

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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{

View File

@ -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
}

View File

@ -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

25
command/config.go Normal file
View File

@ -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,
}
}

View File

@ -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))

View File

@ -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

View File

@ -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{

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
resource "test_instance" "foo" {
ami = "${var.nope}"
}

View File

@ -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
},

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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"
}
}

View File

@ -0,0 +1,6 @@
resource "aws_instance" "web" {
}
resource "aws_instance" "db" {
ami = "${aws_instance.loadbalancer.foo}"
}

View File

@ -0,0 +1,8 @@
variable "foo" {
default = "bar";
description = "bar";
}
provider "do" {
api_key = "${var.bar}";
}

View File

@ -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,
}
}

View File

@ -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))
}

View File

@ -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.

View File

@ -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 {

View File

@ -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)
}
}

605
terraform/context.go Normal file
View File

@ -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
}
}

1000
terraform/context_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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
}

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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
}
*/

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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 {

View File

@ -0,0 +1,8 @@
# Required
variable "foo" {
}
# Optional
variable "bar" {
default = "baz"
}

View File

@ -0,0 +1,5 @@
provider "aws" {
foo = "bar"
}
resource "aws_instance" "test" {}

View File

@ -0,0 +1,3 @@
resource "aws_instance" "test" {
foo = "bar"
}

View File

@ -0,0 +1,7 @@
resource "aws_instance" "foo" {
num = "2"
}
resource "aws_instance" "bar" {
foo = "${var.foo}"
}

View File

@ -0,0 +1,7 @@
resource "aws_instance" "foo" {
num = "2"
}
resource "aws_instance" "bar" {
foo = "bar"
}

View File

@ -0,0 +1,5 @@
variable "foo" {}
resource "aws_instance" "web" {
ami = "${var.foo}"
}