From e250a6f36cf30d2ae601a9be5be25f91e9b6b10b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 2 Jul 2014 20:20:13 -0700 Subject: [PATCH 01/26] config: understand "provisioner" blocks --- config/config.go | 11 +++- config/loader_libucl.go | 84 ++++++++++++++++++++++++++-- config/loader_test.go | 45 +++++++++++++++ config/test-fixtures/provisioners.tf | 11 ++++ 4 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 config/test-fixtures/provisioners.tf diff --git a/config/config.go b/config/config.go index fd6cfba3c..1dd04227c 100644 --- a/config/config.go +++ b/config/config.go @@ -31,9 +31,16 @@ type ProviderConfig struct { // A Terraform resource is something that represents some component that // can be created and managed, and has some properties associated with it. type Resource struct { - Name string + Name string + Type string + Count int + RawConfig *RawConfig + Provisioners []*Provisioner +} + +// Provisioner is a configured provisioner step on a resource. +type Provisioner struct { Type string - Count int RawConfig *RawConfig } diff --git a/config/loader_libucl.go b/config/loader_libucl.go index bf219f0b0..b95190e4e 100644 --- a/config/loader_libucl.go +++ b/config/loader_libucl.go @@ -312,6 +312,10 @@ func loadResourcesLibucl(o *libucl.Object) ([]*Resource, error) { // Remove the "count" from the config, since we treat that special delete(config, "count") + // Delete the "provisioner" section from the config since + // that is treated specially. + delete(config, "provisioner") + rawConfig, err := NewRawConfig(config) if err != nil { return nil, fmt.Errorf( @@ -335,14 +339,86 @@ func loadResourcesLibucl(o *libucl.Object) ([]*Resource, error) { } } + // If we have provisioners, then parse those out + var provisioners []*Provisioner + if po := r.Get("provisioner"); po != nil { + var err error + provisioners, err = loadProvisionersLibucl(po) + po.Close() + if err != nil { + return nil, fmt.Errorf( + "Error reading provisioners for %s[%s]: %s", + t.Key(), + r.Key(), + err) + } + } + result = append(result, &Resource{ - Name: r.Key(), - Type: t.Key(), - Count: count, - RawConfig: rawConfig, + Name: r.Key(), + Type: t.Key(), + Count: count, + RawConfig: rawConfig, + Provisioners: provisioners, }) } } return result, nil } + +func loadProvisionersLibucl(o *libucl.Object) ([]*Provisioner, error) { + pos := make([]*libucl.Object, 0, int(o.Len())) + + // Accumulate all the actual provisioner configuration objects. We + // have to iterate twice here: + // + // 1. The first iteration is of the list of `provisioner` blocks. + // 2. The second iteration is of the dictionary within the + // provisioner which will have only one element which is the + // type of provisioner to use along with tis config. + // + // In JSON it looks kind of like this: + // + // [ + // { + // "shell": { + // ... + // } + // } + // ] + // + iter := o.Iterate(false) + for o1 := iter.Next(); o1 != nil; o1 = iter.Next() { + iter2 := o1.Iterate(true) + for o2 := iter2.Next(); o2 != nil; o2 = iter2.Next() { + pos = append(pos, o2) + } + + o1.Close() + iter2.Close() + } + iter.Close() + + result := make([]*Provisioner, 0, len(pos)) + for _, po := range pos { + defer po.Close() + + var config map[string]interface{} + if err := po.Decode(&config); err != nil { + return nil, err + } + + rawConfig, err := NewRawConfig(config) + if err != nil { + return nil, err + } + + result = append(result, &Provisioner{ + Type: po.Key(), + RawConfig: rawConfig, + }) + } + + return result, nil +} diff --git a/config/loader_test.go b/config/loader_test.go index ba9dadb37..3102dfb73 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -131,6 +131,22 @@ func outputsStr(os map[string]*Output) string { return strings.TrimSpace(result) } +func TestLoad_provisioners(t *testing.T) { + c, err := Load(filepath.Join(fixtureDir, "provisioners.tf")) + if err != nil { + t.Fatalf("err: %s", err) + } + + if c == nil { + t.Fatal("config should not be nil") + } + + actual := resourcesStr(c.Resources) + if actual != strings.TrimSpace(provisionerResourcesStr) { + t.Fatalf("bad:\n%s", actual) + } +} + // This helper turns a provider configs field into a deterministic // string value for comparison in tests. func providerConfigsStr(pcs map[string]*ProviderConfig) string { @@ -213,6 +229,23 @@ func resourcesStr(rs []*Resource) string { result += fmt.Sprintf(" %s\n", k) } + if len(r.Provisioners) > 0 { + result += fmt.Sprintf(" provisioners\n") + for _, p := range r.Provisioners { + result += fmt.Sprintf(" %s\n", p.Type) + + ks := make([]string, 0, len(p.RawConfig.Raw)) + for k, _ := range p.RawConfig.Raw { + ks = append(ks, k) + } + sort.Strings(ks) + + for _, k := range ks { + result += fmt.Sprintf(" %s\n", k) + } + } + } + if len(r.RawConfig.Variables) > 0 { result += fmt.Sprintf(" vars\n") @@ -328,6 +361,18 @@ foo bar ` +const provisionerResourcesStr = ` +aws_instance[web] + ami + security_groups + provisioners + shell + path + vars + resource: aws_security_group.firewall.foo + user: var.foo +` + const variablesVariablesStr = ` bar <> diff --git a/config/test-fixtures/provisioners.tf b/config/test-fixtures/provisioners.tf new file mode 100644 index 000000000..16578134c --- /dev/null +++ b/config/test-fixtures/provisioners.tf @@ -0,0 +1,11 @@ +resource "aws_instance" "web" { + ami = "${var.foo}" + security_groups = [ + "foo", + "${aws_security_group.firewall.foo}" + ] + + provisioner "shell" { + path = "foo" + } +} From 34e733724d8a95e28430b7312ee964dd248c32f8 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 7 Jul 2014 11:35:33 -0700 Subject: [PATCH 02/26] config: Update test to handle count --- config/loader_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/loader_test.go b/config/loader_test.go index 3102dfb73..56e46b272 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -362,7 +362,7 @@ foo ` const provisionerResourcesStr = ` -aws_instance[web] +aws_instance[web] (x1) ami security_groups provisioners From 5a5f1df11507f7c204da321a12ba6840b33c8d21 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 7 Jul 2014 15:08:33 -0700 Subject: [PATCH 03/26] terraform: Adding ResourceConnectionInfo --- terraform/state.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/terraform/state.go b/terraform/state.go index b7daf9ee1..73069f847 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -175,6 +175,21 @@ func WriteState(d *State, dst io.Writer) error { return gob.NewEncoder(dst).Encode(d) } +// ResourceConnectionInfo holds addresses, credentials and configuration +// information require to connect to a resource. This is populated +// by a provider so that provisioners can connect and run on the +// resource. +type ResourceConnectionInfo struct { + // Type is set so that an appropriate connection can be formed. + // As an example, for a Linux machine, the Type may be "ssh" + Type string + + // Raw is used to store any relevant keys for the given Type + // so that a provisioner can connect to the resource. This could + // contain credentials or address information. + Raw map[string]string +} + // ResourceState holds the state of a resource that is used so that // a provider can find and manage an existing resource as well as for // storing attributes that are uesd to populate variables of child @@ -204,6 +219,11 @@ type ResourceState struct { // ${resourcetype.name.attribute}. Attributes map[string]string + // ConnInfo is used for the providers to export information which is + // used to connect to the resource for provisioning. For example, + // this could contain SSH or WinRM credentials. + ConnInfo *ResourceConnectionInfo + // Extra information that the provider can store about a resource. // This data is opaque, never shown to the user, and is sent back to // the provider as-is for whatever purpose appropriate. From d46ca67f92318ec05cbd3fb7b1ce98622a60b1cc Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 7 Jul 2014 15:23:49 -0700 Subject: [PATCH 04/26] terraform: Adding resource provisioner interface --- terraform/resource_provisioner.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 terraform/resource_provisioner.go diff --git a/terraform/resource_provisioner.go b/terraform/resource_provisioner.go new file mode 100644 index 000000000..6c0c03f10 --- /dev/null +++ b/terraform/resource_provisioner.go @@ -0,0 +1,28 @@ +package terraform + +// ResourceProvisioner is an interface that must be implemented by any +// resource provisioner: the thing that initializes resources in +// a Terraform configuration. +type ResourceProvisioner interface { + // Validate is called once at the beginning with the raw + // configuration (no interpolation done) and can return a list of warnings + // and/or errors. + // + // This is called once per resource. + // + // This should not assume any of the values in the resource configuration + // are valid since it is possible they have to be interpolated still. + // The primary use case of this call is to check that the required keys + // are set and that the general structure is correct. + Validate(*ResourceConfig) ([]string, []error) + + // Apply runs the provisioner on a specific resource and returns the new + // resource state along with an error. Instead of a diff, the ResourceConfig + // is provided since provisioners only run after a resource has been + // newly created. + Apply(*ResourceState, *ResourceConfig) (*ResourceState, error) +} + +// ResourceProvisionerFactory is a function type that creates a new instance +// of a resource provisioner. +type ResourceProvisionerFactory func() (ResourceProvisioner, error) From 55124b9e2897d2c2084bc25974b97975444ad061 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 7 Jul 2014 15:24:20 -0700 Subject: [PATCH 05/26] terraform: Adding provisioners to a resource --- terraform/resource.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/terraform/resource.go b/terraform/resource.go index 609a21fe8..eda9f1277 100644 --- a/terraform/resource.go +++ b/terraform/resource.go @@ -13,11 +13,12 @@ import ( // its current state, and potentially a desired diff from the state it // wants to reach. type Resource struct { - Id string - Config *ResourceConfig - Diff *ResourceDiff - Provider ResourceProvider - State *ResourceState + Id string + Config *ResourceConfig + Diff *ResourceDiff + Provider ResourceProvider + State *ResourceState + Provisioners []ResourceProvisioner } // Vars returns the mapping of variables that should be replaced in From b2758666eb74146ca26d318fb4c7675de2930c00 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 7 Jul 2014 15:58:06 -0700 Subject: [PATCH 06/26] terraform: Store resource config along side provisioner --- terraform/resource.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/terraform/resource.go b/terraform/resource.go index eda9f1277..87c146563 100644 --- a/terraform/resource.go +++ b/terraform/resource.go @@ -9,6 +9,16 @@ import ( "github.com/hashicorp/terraform/config" ) +// ResourceProvisionerConfig is used to pair a provisioner +// with it's provided configuration. This allows us to use singleton +// instances of each ResourceProvisioner and to keep the relevant +// configuration instead of instantiating a new Provisioner for each +// resource. +type ResourceProvisionerConfig struct { + Provisioner ResourceProvisioner + Config *ResourceConfig +} + // Resource encapsulates a resource, its configuration, its provider, // its current state, and potentially a desired diff from the state it // wants to reach. @@ -18,7 +28,7 @@ type Resource struct { Diff *ResourceDiff Provider ResourceProvider State *ResourceState - Provisioners []ResourceProvisioner + Provisioners []*ResourceProvisionerConfig } // Vars returns the mapping of variables that should be replaced in From 8901a6753b8a3243c709cfb1ede25504b25b50df Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 7 Jul 2014 16:21:43 -0700 Subject: [PATCH 07/26] terraform: Handle setup of providers in graph construction --- terraform/graph.go | 133 ++++++++++++++++++++++++++++++++++-------- terraform/resource.go | 1 + 2 files changed, 111 insertions(+), 23 deletions(-) diff --git a/terraform/graph.go b/terraform/graph.go index 40c2a63e7..327d01c3e 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -39,6 +39,10 @@ type GraphOpts struct { // This will also potentially insert new nodes into the graph for // the configuration of resource providers. Providers map[string]ResourceProviderFactory + + // Provisioners is a mapping of names to a resource provisioner. + // These must be provided to support resource provisioners. + Provisioners map[string]ResourceProvisionerFactory } // GraphRootNode is the name of the root node in the Terraform resource @@ -110,6 +114,12 @@ func Graph(opts *GraphOpts) (*depgraph.Graph, error) { // Map the provider configurations to all of the resources graphAddProviderConfigs(g, opts.Config) + // Setup the provisioners. These may have variable dependencies, + // and must be done before dependency setup + if err := graphMapResourceProvisioners(g, opts.Provisioners); err != nil { + return nil, err + } + // Add all the variable dependencies graphAddVariableDeps(g) @@ -523,37 +533,55 @@ func graphAddVariableDeps(g *depgraph.Graph) { var vars map[string]config.InterpolatedVariable switch m := n.Meta.(type) { case *GraphNodeResource: - if !m.Orphan { - vars = m.Config.RawConfig.Variables + // Ignore orphan nodes + if m.Orphan { + continue } + + // Handle the resource variables + vars = m.Config.RawConfig.Variables + nounAddVariableDeps(g, n, vars) + + // Handle the variables of the resource provisioners + for _, p := range m.Resource.Provisioners { + vars = p.RawConfig.Variables + nounAddVariableDeps(g, n, vars) + } + case *GraphNodeResourceProvider: vars = m.Config.RawConfig.Variables + nounAddVariableDeps(g, n, vars) + default: continue } + } +} - for _, v := range vars { - // Only resource variables impose dependencies - rv, ok := v.(*config.ResourceVariable) - if !ok { - continue - } - - // Find the target - target := g.Noun(rv.ResourceId()) - if target == nil { - continue - } - - // Build the dependency - dep := &depgraph.Dependency{ - Name: rv.ResourceId(), - Source: n, - Target: target, - } - - n.Deps = append(n.Deps, dep) +// nounAddVariableDeps updates the dependencies of a noun given +// a set of associated variable values +func nounAddVariableDeps(g *depgraph.Graph, n *depgraph.Noun, vars map[string]config.InterpolatedVariable) { + for _, v := range vars { + // Only resource variables impose dependencies + rv, ok := v.(*config.ResourceVariable) + if !ok { + continue } + + // Find the target + target := g.Noun(rv.ResourceId()) + if target == nil { + continue + } + + // Build the dependency + dep := &depgraph.Dependency{ + Name: rv.ResourceId(), + Source: n, + Target: target, + } + + n.Deps = append(n.Deps, dep) } } @@ -691,6 +719,65 @@ func graphMapResourceProviders(g *depgraph.Graph) error { return nil } +// graphMapResourceProvisioners takes a graph that already has +// the resources and maps the resource provisioners to the resources themselves. +func graphMapResourceProvisioners(g *depgraph.Graph, + provisioners map[string]ResourceProvisionerFactory) error { + var errs []error + + // Create a cache of resource provisioners, avoids duplicate + // initialization of the instances + cache := make(map[string]ResourceProvisioner) + + // Go through each of the resources and find a matching provisioners + for _, n := range g.Nouns { + rn, ok := n.Meta.(*GraphNodeResource) + if !ok { + continue + } + + // Check each provisioner + for _, p := range rn.Config.Provisioners { + // Check for a cached provisioner + provisioner, ok := cache[p.Type] + if !ok { + // Lookup the factory method + factory, ok := provisioners[p.Type] + if !ok { + errs = append(errs, fmt.Errorf( + "Resource provisioner not found for provisioner type '%s'", + p.Type)) + continue + } + + // Initialize the provisioner + prov, err := factory() + if err != nil { + errs = append(errs, fmt.Errorf( + "Failed to instantiate provisioner type '%s': %v", + p.Type, err)) + continue + } + provisioner = prov + + // Cache this type of provisioner + cache[p.Type] = prov + } + + // Save the provisioner + rn.Resource.Provisioners = append(rn.Resource.Provisioners, &ResourceProvisionerConfig{ + Provisioner: provisioner, + RawConfig: p.RawConfig, + }) + } + } + + if len(errs) > 0 { + return &multierror.Error{Errors: errs} + } + return nil +} + // matchingPrefixes takes a resource type and a set of resource // providers we know about by prefix and returns a list of prefixes // that might be valid for that resource. diff --git a/terraform/resource.go b/terraform/resource.go index 87c146563..5d7aaee4d 100644 --- a/terraform/resource.go +++ b/terraform/resource.go @@ -17,6 +17,7 @@ import ( type ResourceProvisionerConfig struct { Provisioner ResourceProvisioner Config *ResourceConfig + RawConfig *config.RawConfig } // Resource encapsulates a resource, its configuration, its provider, From 9fc641377590814efde67651e9e08ed8350d4d22 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 8 Jul 2014 10:52:59 -0700 Subject: [PATCH 08/26] terraform: Ignore orphans in provisioner setup --- terraform/graph.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/terraform/graph.go b/terraform/graph.go index 327d01c3e..d1fdf43c6 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -736,6 +736,11 @@ func graphMapResourceProvisioners(g *depgraph.Graph, continue } + // Ignore orphan nodes with no provisioners + if rn.Config == nil { + continue + } + // Check each provisioner for _, p := range rn.Config.Provisioners { // Check for a cached provisioner From 975ff45149955a68615446a875533ab4336f0799 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 8 Jul 2014 11:01:22 -0700 Subject: [PATCH 09/26] terraform: Adding mock resource provisioner --- terraform/resource_provisioner_mock.go | 36 +++++++++++++++++++++ terraform/resource_provisioner_mock_test.go | 9 ++++++ 2 files changed, 45 insertions(+) create mode 100644 terraform/resource_provisioner_mock.go create mode 100644 terraform/resource_provisioner_mock_test.go diff --git a/terraform/resource_provisioner_mock.go b/terraform/resource_provisioner_mock.go new file mode 100644 index 000000000..e50342416 --- /dev/null +++ b/terraform/resource_provisioner_mock.go @@ -0,0 +1,36 @@ +package terraform + +// MockResourceProvisioner implements ResourceProvisioner but mocks out all the +// calls for testing purposes. +type MockResourceProvisioner struct { + // Anything you want, in case you need to store extra data with the mock. + Meta interface{} + + ApplyCalled bool + ApplyState *ResourceState + ApplyConfig *ResourceConfig + ApplyFn func(*ResourceState, *ResourceConfig) (*ResourceState, error) + ApplyReturn *ResourceState + ApplyReturnError error + + ValidateCalled bool + ValidateConfig *ResourceConfig + ValidateReturnWarns []string + ValidateReturnErrors []error +} + +func (p *MockResourceProvisioner) Validate(c *ResourceConfig) ([]string, []error) { + p.ValidateCalled = true + p.ValidateConfig = c + return p.ValidateReturnWarns, p.ValidateReturnErrors +} + +func (p *MockResourceProvisioner) Apply(state *ResourceState, c *ResourceConfig) (*ResourceState, error) { + p.ApplyCalled = true + p.ApplyState = state + p.ApplyConfig = c + if p.ApplyFn != nil { + return p.ApplyFn(state, c) + } + return p.ApplyReturn, p.ApplyReturnError +} diff --git a/terraform/resource_provisioner_mock_test.go b/terraform/resource_provisioner_mock_test.go new file mode 100644 index 000000000..57795b425 --- /dev/null +++ b/terraform/resource_provisioner_mock_test.go @@ -0,0 +1,9 @@ +package terraform + +import ( + "testing" +) + +func TestMockResourceProvisioner_impl(t *testing.T) { + var _ ResourceProvisioner = new(MockResourceProvisioner) +} From e8245f1a67ecbea3a03c196f2712fb2b1125b3f0 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 8 Jul 2014 12:42:50 -0700 Subject: [PATCH 10/26] terraform: Testing graph construction with provisioners --- terraform/graph_test.go | 74 +++++++++++++++++++ terraform/terraform_test.go | 6 ++ .../test-fixtures/graph-provisioners/main.tf | 28 +++++++ 3 files changed, 108 insertions(+) create mode 100644 terraform/test-fixtures/graph-provisioners/main.tf diff --git a/terraform/graph_test.go b/terraform/graph_test.go index b634b9cce..d1ef90c3b 100644 --- a/terraform/graph_test.go +++ b/terraform/graph_test.go @@ -128,6 +128,80 @@ func TestGraphFull(t *testing.T) { } } +func TestGraphProvisioners(t *testing.T) { + rpAws := new(MockResourceProvider) + provShell := new(MockResourceProvisioner) + provWinRM := new(MockResourceProvisioner) + + rpAws.ResourcesReturn = []ResourceType{ + ResourceType{Name: "aws_instance"}, + ResourceType{Name: "aws_load_balancer"}, + ResourceType{Name: "aws_security_group"}, + } + + ps := map[string]ResourceProvisionerFactory{ + "shell": testProvisionerFuncFixed(provShell), + "winrm": testProvisionerFuncFixed(provWinRM), + } + + pf := map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(rpAws), + } + + c := testConfig(t, "graph-provisioners") + g, err := Graph(&GraphOpts{Config: c, Providers: pf, Provisioners: ps}) + if err != nil { + t.Fatalf("err: %s", err) + } + + // A helper to help get us the provider for a resource. + graphProvisioner := func(n string, idx int) *ResourceProvisionerConfig { + return g.Noun(n).Meta.(*GraphNodeResource).Resource.Provisioners[idx] + } + + // A helper to verify depedencies + depends := func(a, b string) bool { + aNoun := g.Noun(a) + bNoun := g.Noun(b) + for _, dep := range aNoun.Deps { + if dep.Source == aNoun && dep.Target == bNoun { + return true + } + } + return false + } + + // Test a couple + prov := graphProvisioner("aws_instance.web", 0) + if prov.Provisioner != provWinRM { + t.Fatalf("bad: %#v", prov) + } + if prov.RawConfig.Config()["cmd"] != "echo foo" { + t.Fatalf("bad: %#v", prov) + } + + prov = graphProvisioner("aws_instance.web", 1) + if prov.Provisioner != provWinRM { + t.Fatalf("bad: %#v", prov) + } + if prov.RawConfig.Config()["cmd"] != "echo bar" { + t.Fatalf("bad: %#v", prov) + } + + prov = graphProvisioner("aws_load_balancer.weblb", 0) + if prov.Provisioner != provShell { + t.Fatalf("bad: %#v", prov) + } + if prov.RawConfig.Config()["cmd"] != "add ${aws_instance.web.id}" { + t.Fatalf("bad: %#v", prov) + } + + // Check that the variable dependency is handled + if !depends("aws_load_balancer.weblb", "aws_instance.web") { + t.Fatalf("missing dependency from provisioner variable") + } +} + func TestGraphAddDiff(t *testing.T) { config := testConfig(t, "graph-diff") diff := &Diff{ diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 77a4ded34..74a72a5cc 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -45,6 +45,12 @@ func testProviderFuncFixed(rp ResourceProvider) ResourceProviderFactory { } } +func testProvisionerFuncFixed(rp ResourceProvisioner) ResourceProvisionerFactory { + return func() (ResourceProvisioner, error) { + return rp, nil + } +} + // 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/graph-provisioners/main.tf b/terraform/test-fixtures/graph-provisioners/main.tf new file mode 100644 index 000000000..96035ecc4 --- /dev/null +++ b/terraform/test-fixtures/graph-provisioners/main.tf @@ -0,0 +1,28 @@ +variable "foo" { + default = "bar"; + description = "bar"; +} + +provider "aws" {} + +resource "aws_security_group" "firewall" {} + +resource "aws_instance" "web" { + ami = "${var.foo}" + security_groups = [ + "foo", + "${aws_security_group.firewall.foo}" + ] + provisioner "winrm" { + cmd = "echo foo" + } + provisioner "winrm" { + cmd = "echo bar" + } +} + +resource "aws_load_balancer" "weblb" { + provisioner "shell" { + cmd = "add ${aws_instance.web.id}" + } +} From ee475e817863ed7c8a2503e280ede1885b054bf3 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 8 Jul 2014 14:01:27 -0700 Subject: [PATCH 11/26] terraform: Apply and Validate provisioners when walking --- terraform/context.go | 52 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/terraform/context.go b/terraform/context.go index 0babb455e..97a86624c 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -480,6 +480,16 @@ func (c *Context) applyWalkFn() depgraph.WalkFunc { } } + // Invoke any provisioners we have defined. This is only done + // if the resource was created, as updates or deletes do not + // invoke provisioners. + if r.State.ID == "" && len(r.Provisioners) > 0 { + rs, err = c.applyProvisioners(r, rs) + if err != nil { + errs = append(errs, err) + } + } + // Update the resulting diff c.sl.Lock() if rs.ID == "" { @@ -508,6 +518,26 @@ func (c *Context) applyWalkFn() depgraph.WalkFunc { return c.genericWalkFn(cb) } +// applyProvisioners is used to run any provisioners a resource has +// defined after the resource creation has already completed. +func (c *Context) applyProvisioners(r *Resource, rs *ResourceState) (*ResourceState, error) { + var err error + for _, prov := range r.Provisioners { + // Interpolate since we may have variables that depend on the + // local resource. + if err := r.Config.interpolate(c); err != nil { + return rs, err + } + + // Invoke the Provisioner + rs, err = prov.Provisioner.Apply(rs, r.Config) + if err != nil { + return rs, err + } + } + return rs, nil +} + func (c *Context) planWalkFn(result *Plan) depgraph.WalkFunc { var l sync.Mutex @@ -677,9 +707,21 @@ func (c *Context) validateWalkFn(rws *[]string, res *[]error) depgraph.WalkFunc for i, e := range es { es[i] = fmt.Errorf("'%s' error: %s", rn.Resource.Id, e) } - *rws = append(*rws, ws...) *res = append(*res, es...) + + for idx, p := range rn.Resource.Provisioners { + ws, es := p.Provisioner.Validate(p.Config) + for i, w := range ws { + ws[i] = fmt.Sprintf("'%s.provisioner.%d' warning: %s", rn.Resource.Id, idx, w) + } + for i, e := range es { + es[i] = fmt.Errorf("'%s.provisioner.%d' error: %s", rn.Resource.Id, idx, e) + } + *rws = append(*rws, ws...) + *res = append(*res, es...) + } + case *GraphNodeResourceProvider: if rn.Config == nil { return nil @@ -765,6 +807,14 @@ func (c *Context) genericWalkFn(cb genericWalkFunc) depgraph.WalkFunc { } else { rn.Resource.Config = NewResourceConfig(rn.Config.RawConfig) } + + for _, prov := range rn.Resource.Provisioners { + if prov.RawConfig == nil { + prov.Config = new(ResourceConfig) + } else { + prov.Config = NewResourceConfig(prov.RawConfig) + } + } } else { rn.Resource.Config = nil } From 87c3423fd4526d44d741bc31d4adc968802a5930 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 8 Jul 2014 14:45:45 -0700 Subject: [PATCH 12/26] terrform: Thread provisioner factory through Context --- terraform/context.go | 73 ++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/terraform/context.go b/terraform/context.go index 97a86624c..308ea9127 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -22,12 +22,13 @@ type genericWalkFunc func(*Resource) error // // 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 + config *config.Config + diff *Diff + hooks []Hook + state *State + providers map[string]ResourceProviderFactory + provisioners map[string]ResourceProvisionerFactory + variables map[string]string l sync.Mutex // Lock acquired during any task parCh chan struct{} // Semaphore used to limit parallelism @@ -39,13 +40,14 @@ type Context struct { // ContextOpts are the user-creatable configuration structure to create // a context with NewContext. type ContextOpts struct { - Config *config.Config - Diff *Diff - Hooks []Hook - Parallelism int - State *State - Providers map[string]ResourceProviderFactory - Variables map[string]string + Config *config.Config + Diff *Diff + Hooks []Hook + Parallelism int + State *State + Providers map[string]ResourceProviderFactory + Provisioners map[string]ResourceProvisionerFactory + Variables map[string]string } // NewContext creates a new context. @@ -70,12 +72,13 @@ func NewContext(opts *ContextOpts) *Context { parCh := make(chan struct{}, par) return &Context{ - config: opts.Config, - diff: opts.Diff, - hooks: hooks, - state: opts.State, - providers: opts.Providers, - variables: opts.Variables, + config: opts.Config, + diff: opts.Diff, + hooks: hooks, + state: opts.State, + providers: opts.Providers, + provisioners: opts.Provisioners, + variables: opts.Variables, parCh: parCh, sh: sh, @@ -92,10 +95,11 @@ func (c *Context) Apply() (*State, error) { defer c.releaseRun(v) g, err := Graph(&GraphOpts{ - Config: c.config, - Diff: c.diff, - Providers: c.providers, - State: c.state, + Config: c.config, + Diff: c.diff, + Providers: c.providers, + Provisioners: c.provisioners, + State: c.state, }) if err != nil { return nil, err @@ -135,9 +139,10 @@ func (c *Context) Plan(opts *PlanOpts) (*Plan, error) { defer c.releaseRun(v) g, err := Graph(&GraphOpts{ - Config: c.config, - Providers: c.providers, - State: c.state, + Config: c.config, + Providers: c.providers, + Provisioners: c.provisioners, + State: c.state, }) if err != nil { return nil, err @@ -188,9 +193,10 @@ func (c *Context) Refresh() (*State, error) { defer c.releaseRun(v) g, err := Graph(&GraphOpts{ - Config: c.config, - Providers: c.providers, - State: c.state, + Config: c.config, + Providers: c.providers, + Provisioners: c.provisioners, + State: c.state, }) if err != nil { return c.state, err @@ -384,10 +390,11 @@ func (c *Context) computeResourceMultiVariable( func (c *Context) graph() (*depgraph.Graph, error) { return Graph(&GraphOpts{ - Config: c.config, - Diff: c.diff, - Providers: c.providers, - State: c.state, + Config: c.config, + Diff: c.diff, + Providers: c.providers, + Provisioners: c.provisioners, + State: c.state, }) } From 03a20f072e493b62a0fbd8b4f34761a0810d25cb Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 8 Jul 2014 14:45:59 -0700 Subject: [PATCH 13/26] terraform: Test Validation of provisioners --- terraform/context_test.go | 53 +++++++++++++++++++ .../validate-bad-prov-conf/main.tf | 7 +++ 2 files changed, 60 insertions(+) create mode 100644 terraform/test-fixtures/validate-bad-prov-conf/main.tf diff --git a/terraform/context_test.go b/terraform/context_test.go index c591390d1..3b1310b47 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -132,6 +132,54 @@ func TestContextValidate_requiredVar(t *testing.T) { } } +func TestContextValidate_provisionerConfig_bad(t *testing.T) { + config := testConfig(t, "validate-bad-prov-conf") + p := testProvider("aws") + pr := testProvisioner() + c := testContext(t, &ContextOpts{ + Config: config, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Provisioners: map[string]ResourceProvisionerFactory{ + "shell": testProvisionerFuncFixed(pr), + }, + }) + + pr.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_provisionerConfig_good(t *testing.T) { + config := testConfig(t, "validate-bad-prov-conf") + p := testProvider("aws") + pr := testProvisioner() + c := testContext(t, &ContextOpts{ + Config: config, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Provisioners: map[string]ResourceProvisionerFactory{ + "shell": testProvisionerFuncFixed(pr), + }, + }) + + 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") @@ -1406,3 +1454,8 @@ func testProvider(prefix string) *MockResourceProvider { return p } + +func testProvisioner() *MockResourceProvisioner { + p := new(MockResourceProvisioner) + return p +} diff --git a/terraform/test-fixtures/validate-bad-prov-conf/main.tf b/terraform/test-fixtures/validate-bad-prov-conf/main.tf new file mode 100644 index 000000000..bb239fad7 --- /dev/null +++ b/terraform/test-fixtures/validate-bad-prov-conf/main.tf @@ -0,0 +1,7 @@ +provider "aws" { + foo = "bar" +} + +resource "aws_instance" "test" { + provisioner "shell" {} +} From cfc7b69bb1a6cfe05cbbbeefdffb34b907a1b32d Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 9 Jul 2014 10:36:49 -0700 Subject: [PATCH 14/26] terraform: Test provisioner apply --- terraform/context_test.go | 42 +++++++++++++++++++ terraform/terraform_test.go | 10 +++++ .../apply-provisioner-compute/main.tf | 11 +++++ 3 files changed, 63 insertions(+) create mode 100644 terraform/test-fixtures/apply-provisioner-compute/main.tf diff --git a/terraform/context_test.go b/terraform/context_test.go index 3b1310b47..906bff7d9 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -339,6 +339,48 @@ func TestContextApply_compute(t *testing.T) { } } +func TestContextApply_Provisioner_compute(t *testing.T) { + c := testConfig(t, "apply-provisioner-compute") + p := testProvider("aws") + pr := testProvisioner() + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + pr.ApplyFn = func(rs *ResourceState, c *ResourceConfig) (*ResourceState, error) { + return rs, nil + } + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Provisioners: map[string]ResourceProvisionerFactory{ + "shell": testProvisionerFuncFixed(pr), + }, + }) + + 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(testTerraformApplyProvisionerStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } + + // Verify apply was invoked + if !pr.ApplyCalled { + t.Fatalf("provisioner not invoked") + } +} + func TestContextApply_destroy(t *testing.T) { c := testConfig(t, "apply-destroy") h := new(HookRecordApplyOrder) diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 74a72a5cc..203741ed7 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -120,6 +120,16 @@ aws_instance.foo: ID = foo ` +const testTerraformApplyProvisionerStr = ` +aws_instance.bar: + ID = foo +aws_instance.foo: + ID = foo + dynamical = computed_dynamical + num = 2 + type = aws_instance +` + const testTerraformApplyDestroyStr = ` ` diff --git a/terraform/test-fixtures/apply-provisioner-compute/main.tf b/terraform/test-fixtures/apply-provisioner-compute/main.tf new file mode 100644 index 000000000..e0ff5507a --- /dev/null +++ b/terraform/test-fixtures/apply-provisioner-compute/main.tf @@ -0,0 +1,11 @@ +resource "aws_instance" "foo" { + num = "2" + compute = "dynamical" + compute_value = "${var.value}" +} + +resource "aws_instance" "bar" { + provisioner "shell" { + foo = "${aws_instance.foo.dynamical}" + } +} From 3849ca80efc6d62f316caa602b52a8e08f1d1b83 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 9 Jul 2014 11:03:19 -0700 Subject: [PATCH 15/26] plugin: Adding support for provisioners --- plugin/plugin_test.go | 6 +++++ plugin/resource_provisioner.go | 35 +++++++++++++++++++++++++++++ plugin/resource_provisioner_test.go | 23 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 plugin/resource_provisioner.go create mode 100644 plugin/resource_provisioner_test.go diff --git a/plugin/plugin_test.go b/plugin/plugin_test.go index 038bc3917..7e5dcfcb1 100644 --- a/plugin/plugin_test.go +++ b/plugin/plugin_test.go @@ -60,6 +60,12 @@ func TestHelperProcess(*testing.T) { log.Printf("[ERR] %s", err) os.Exit(1) } + case "resource-provisioner": + err := Serve(new(terraform.MockResourceProvisioner)) + if err != nil { + log.Printf("[ERR] %s", err) + os.Exit(1) + } case "invalid-rpc-address": fmt.Println("lolinvalid") case "mock": diff --git a/plugin/resource_provisioner.go b/plugin/resource_provisioner.go new file mode 100644 index 000000000..6d8fd39db --- /dev/null +++ b/plugin/resource_provisioner.go @@ -0,0 +1,35 @@ +package plugin + +import ( + "os/exec" + + tfrpc "github.com/hashicorp/terraform/rpc" + "github.com/hashicorp/terraform/terraform" +) + +// ResourceProvisionerFactory returns a Terraform ResourceProvisionerFactory +// that executes a plugin and connects to it. +func ResourceProvisionerFactory(cmd *exec.Cmd) terraform.ResourceProvisionerFactory { + return func() (terraform.ResourceProvisioner, error) { + config := &ClientConfig{ + Cmd: cmd, + Managed: true, + } + + client := NewClient(config) + rpcClient, err := client.Client() + if err != nil { + return nil, err + } + + rpcName, err := client.Service() + if err != nil { + return nil, err + } + + return &tfrpc.ResourceProvisioner{ + Client: rpcClient, + Name: rpcName, + }, nil + } +} diff --git a/plugin/resource_provisioner_test.go b/plugin/resource_provisioner_test.go new file mode 100644 index 000000000..2ca37c7d9 --- /dev/null +++ b/plugin/resource_provisioner_test.go @@ -0,0 +1,23 @@ +package plugin + +import ( + "testing" +) + +func TestResourceProvisioner(t *testing.T) { + c := NewClient(&ClientConfig{Cmd: helperProcess("resource-provisioner")}) + defer c.Kill() + + _, err := c.Client() + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + service, err := c.Service() + if err != nil { + t.Fatalf("err: %s", err) + } + if service == "" { + t.Fatal("service should not be blank") + } +} From c952513aed919dfba9065eb0a983818cb267ccf0 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 9 Jul 2014 11:13:05 -0700 Subject: [PATCH 16/26] rpc: Adding support for provisioners --- rpc/resource_provisioner.go | 106 ++++++++++++++++++++++++ rpc/resource_provisioner_test.go | 138 +++++++++++++++++++++++++++++++ rpc/rpc.go | 3 + 3 files changed, 247 insertions(+) create mode 100644 rpc/resource_provisioner.go create mode 100644 rpc/resource_provisioner_test.go diff --git a/rpc/resource_provisioner.go b/rpc/resource_provisioner.go new file mode 100644 index 000000000..dc8a0ac16 --- /dev/null +++ b/rpc/resource_provisioner.go @@ -0,0 +1,106 @@ +package rpc + +import ( + "github.com/hashicorp/terraform/terraform" + "net/rpc" +) + +// ResourceProvisioner is an implementation of terraform.ResourceProvisioner +// that communicates over RPC. +type ResourceProvisioner struct { + Client *rpc.Client + Name string +} + +func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) ([]string, []error) { + var resp ResourceProvisionerValidateResponse + args := ResourceProvisionerValidateArgs{ + Config: c, + } + + err := p.Client.Call(p.Name+".Validate", &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 *ResourceProvisioner) Apply( + s *terraform.ResourceState, + c *terraform.ResourceConfig) (*terraform.ResourceState, error) { + var resp ResourceProvisionerApplyResponse + args := &ResourceProvisionerApplyArgs{ + State: s, + Config: c, + } + + err := p.Client.Call(p.Name+".Apply", args, &resp) + if err != nil { + return nil, err + } + if resp.Error != nil { + err = resp.Error + } + + return resp.State, err +} + +type ResourceProvisionerValidateArgs struct { + Config *terraform.ResourceConfig +} + +type ResourceProvisionerValidateResponse struct { + Warnings []string + Errors []*BasicError +} + +type ResourceProvisionerApplyArgs struct { + State *terraform.ResourceState + Config *terraform.ResourceConfig +} + +type ResourceProvisionerApplyResponse struct { + State *terraform.ResourceState + Error *BasicError +} + +// ResourceProvisionerServer is a net/rpc compatible structure for serving +// a ResourceProvisioner. This should not be used directly. +type ResourceProvisionerServer struct { + Provisioner terraform.ResourceProvisioner +} + +func (s *ResourceProvisionerServer) Apply( + args *ResourceProvisionerApplyArgs, + result *ResourceProvisionerApplyResponse) error { + state, err := s.Provisioner.Apply(args.State, args.Config) + *result = ResourceProvisionerApplyResponse{ + State: state, + Error: NewBasicError(err), + } + return nil +} + +func (s *ResourceProvisionerServer) Validate( + args *ResourceProvisionerValidateArgs, + reply *ResourceProvisionerValidateResponse) error { + warns, errs := s.Provisioner.Validate(args.Config) + berrs := make([]*BasicError, len(errs)) + for i, err := range errs { + berrs[i] = NewBasicError(err) + } + *reply = ResourceProvisionerValidateResponse{ + Warnings: warns, + Errors: berrs, + } + return nil +} diff --git a/rpc/resource_provisioner_test.go b/rpc/resource_provisioner_test.go new file mode 100644 index 000000000..73f11eb9a --- /dev/null +++ b/rpc/resource_provisioner_test.go @@ -0,0 +1,138 @@ +package rpc + +import ( + "errors" + "reflect" + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +func TestResourceProvisioner_impl(t *testing.T) { + var _ terraform.ResourceProvisioner = new(ResourceProvisioner) +} + +func TestResourceProvisioner_apply(t *testing.T) { + p := new(terraform.MockResourceProvisioner) + client, server := testClientServer(t) + name, err := Register(server, p) + if err != nil { + t.Fatalf("err: %s", err) + } + provisioner := &ResourceProvisioner{Client: client, Name: name} + + p.ApplyReturn = &terraform.ResourceState{ + ID: "bob", + } + + // Apply + state := &terraform.ResourceState{} + conf := &terraform.ResourceConfig{} + newState, err := provisioner.Apply(state, conf) + if !p.ApplyCalled { + t.Fatal("apply should be called") + } + if !reflect.DeepEqual(p.ApplyConfig, conf) { + t.Fatalf("bad: %#v", p.ApplyConfig) + } + if err != nil { + t.Fatalf("bad: %#v", err) + } + if !reflect.DeepEqual(p.ApplyReturn, newState) { + t.Fatalf("bad: %#v", newState) + } +} + +func TestResourceProvisioner_validate(t *testing.T) { + p := new(terraform.MockResourceProvisioner) + client, server := testClientServer(t) + name, err := Register(server, p) + if err != nil { + t.Fatalf("err: %s", err) + } + provisioner := &ResourceProvisioner{Client: client, Name: name} + + // Configure + config := &terraform.ResourceConfig{ + Raw: map[string]interface{}{"foo": "bar"}, + } + w, e := provisioner.Validate(config) + if !p.ValidateCalled { + t.Fatal("configure should be called") + } + if !reflect.DeepEqual(p.ValidateConfig, config) { + t.Fatalf("bad: %#v", p.ValidateConfig) + } + if w != nil { + t.Fatalf("bad: %#v", w) + } + if e != nil { + t.Fatalf("bad: %#v", e) + } +} + +func TestResourceProvisioner_validate_errors(t *testing.T) { + p := new(terraform.MockResourceProvisioner) + p.ValidateReturnErrors = []error{errors.New("foo")} + + client, server := testClientServer(t) + name, err := Register(server, p) + if err != nil { + t.Fatalf("err: %s", err) + } + provisioner := &ResourceProvisioner{Client: client, Name: name} + + // Configure + config := &terraform.ResourceConfig{ + Raw: map[string]interface{}{"foo": "bar"}, + } + w, e := provisioner.Validate(config) + if !p.ValidateCalled { + t.Fatal("configure should be called") + } + if !reflect.DeepEqual(p.ValidateConfig, config) { + t.Fatalf("bad: %#v", p.ValidateConfig) + } + 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 TestResourceProvisioner_validate_warns(t *testing.T) { + p := new(terraform.MockResourceProvisioner) + p.ValidateReturnWarns = []string{"foo"} + + client, server := testClientServer(t) + name, err := Register(server, p) + if err != nil { + t.Fatalf("err: %s", err) + } + provisioner := &ResourceProvisioner{Client: client, Name: name} + + // Configure + config := &terraform.ResourceConfig{ + Raw: map[string]interface{}{"foo": "bar"}, + } + w, e := provisioner.Validate(config) + if !p.ValidateCalled { + t.Fatal("configure should be called") + } + if !reflect.DeepEqual(p.ValidateConfig, config) { + t.Fatalf("bad: %#v", p.ValidateConfig) + } + if e != nil { + t.Fatalf("bad: %#v", e) + } + + expected := []string{"foo"} + if !reflect.DeepEqual(w, expected) { + t.Fatalf("bad: %#v", w) + } +} diff --git a/rpc/rpc.go b/rpc/rpc.go index 5f178ae93..f11a482f3 100644 --- a/rpc/rpc.go +++ b/rpc/rpc.go @@ -23,6 +23,9 @@ func Register(server *rpc.Server, thing interface{}) (name string, err error) { case terraform.ResourceProvider: name = fmt.Sprintf("Terraform%d", nextId) err = server.RegisterName(name, &ResourceProviderServer{Provider: t}) + case terraform.ResourceProvisioner: + name = fmt.Sprintf("Terraform%d", nextId) + err = server.RegisterName(name, &ResourceProvisionerServer{Provisioner: t}) default: return "", errors.New("Unknown type to register for RPC server.") } From 9c49642b376020fdc8a8ff48c1f3881a03b41c79 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 9 Jul 2014 13:34:08 -0700 Subject: [PATCH 17/26] provisioner/local-exec: First pass --- builtin/bins/provisioner-local-exec/main.go | 10 +++ .../bins/provisioner-local-exec/main_test.go | 1 + .../local-exec/resource_provisioner.go | 65 +++++++++++++++++++ .../local-exec/resource_provisioner_test.go | 11 ++++ 4 files changed, 87 insertions(+) create mode 100644 builtin/bins/provisioner-local-exec/main.go create mode 100644 builtin/bins/provisioner-local-exec/main_test.go create mode 100644 builtin/provisioners/local-exec/resource_provisioner.go create mode 100644 builtin/provisioners/local-exec/resource_provisioner_test.go diff --git a/builtin/bins/provisioner-local-exec/main.go b/builtin/bins/provisioner-local-exec/main.go new file mode 100644 index 000000000..eb697e57b --- /dev/null +++ b/builtin/bins/provisioner-local-exec/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/provisioners/local-exec" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(new(localexec.ResourceProvisioner)) +} diff --git a/builtin/bins/provisioner-local-exec/main_test.go b/builtin/bins/provisioner-local-exec/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provisioner-local-exec/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/provisioners/local-exec/resource_provisioner.go b/builtin/provisioners/local-exec/resource_provisioner.go new file mode 100644 index 000000000..7c8e01230 --- /dev/null +++ b/builtin/provisioners/local-exec/resource_provisioner.go @@ -0,0 +1,65 @@ +package localexec + +import ( + "fmt" + "os/exec" + "runtime" + + "github.com/armon/circbuf" + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/terraform" +) + +const ( + // maxBufSize limits how much output we collect from a local + // invocation. This is to prevent TF memory usage from growing + // to an enormous amount due to a faulty process. + maxBufSize = 8 * 1024 +) + +type ResourceProvisioner struct{} + +func (p *ResourceProvisioner) Apply( + s *terraform.ResourceState, + c *terraform.ResourceConfig) (*terraform.ResourceState, error) { + + // Get the command + commandRaw, ok := c.Get("command") + if !ok { + return s, fmt.Errorf("local-exec provisioner missing 'command'") + } + command, ok := commandRaw.(string) + if !ok { + return s, fmt.Errorf("local-exec provisioner command must be a string") + } + + // Execute the command using a shell + var shell, flag string + if runtime.GOOS == "windows" { + shell = "cmd" + flag = "/C" + } else { + shell = "/bin/sh" + flag = "-c" + } + + // Setup the command + cmd := exec.Command(shell, flag, command) + output, _ := circbuf.NewBuffer(maxBufSize) + cmd.Stderr = output + cmd.Stdout = output + + // Run the command to completion + if err := cmd.Run(); err != nil { + return s, fmt.Errorf("Error running command '%s': %v. Output: %s", + command, err, output.Bytes()) + } + return s, nil +} + +func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) ([]string, []error) { + validator := config.Validator{ + Required: []string{"command"}, + } + return validator.Validate(c) +} diff --git a/builtin/provisioners/local-exec/resource_provisioner_test.go b/builtin/provisioners/local-exec/resource_provisioner_test.go new file mode 100644 index 000000000..a066fc996 --- /dev/null +++ b/builtin/provisioners/local-exec/resource_provisioner_test.go @@ -0,0 +1,11 @@ +package localexec + +import ( + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +func TestResourceProvisioner_impl(t *testing.T) { + var _ terraform.ResourceProvisioner = new(ResourceProvisioner) +} From 1c4321a50354cdb92ddfca0e69d142cf8a37fc3c Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 9 Jul 2014 14:47:37 -0700 Subject: [PATCH 18/26] Setup provisioners for CLI --- config.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- main.go | 1 + 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/config.go b/config.go index c3cfb0f9f..b616414d7 100644 --- a/config.go +++ b/config.go @@ -16,7 +16,8 @@ import ( // This is not the configuration for Terraform itself. That is in the // "config" package. type Config struct { - Providers map[string]string + Providers map[string]string + Provisioners map[string]string } // BuiltinConfig is the built-in defaults for the configuration. These @@ -34,6 +35,9 @@ func init() { BuiltinConfig.Providers = map[string]string{ "aws": "terraform-provider-aws", } + BuiltinConfig.Provisioners = map[string]string{ + "local-exec": "terraform-provisioner-local-exec", + } } // LoadConfig loads the CLI configuration from ".terraformrc" files. @@ -69,12 +73,19 @@ func LoadConfig(path string) (*Config, error) { func (c1 *Config) Merge(c2 *Config) *Config { var result Config result.Providers = make(map[string]string) + result.Provisioners = make(map[string]string) for k, v := range c1.Providers { result.Providers[k] = v } for k, v := range c2.Providers { result.Providers[k] = v } + for k, v := range c1.Provisioners { + result.Provisioners[k] = v + } + for k, v := range c2.Provisioners { + result.Provisioners[k] = v + } return &result } @@ -138,3 +149,63 @@ func (c *Config) providerFactory(path string) terraform.ResourceProviderFactory }, nil } } + +// ProvisionerFactories returns the mapping of prefixes to +// ResourceProvisionerFactory that can be used to instantiate a +// binary-based plugin. +func (c *Config) ProvisionerFactories() map[string]terraform.ResourceProvisionerFactory { + result := make(map[string]terraform.ResourceProvisionerFactory) + for k, v := range c.Provisioners { + result[k] = c.provisionerFactory(v) + } + + return result +} + +func (c *Config) provisionerFactory(path string) terraform.ResourceProvisionerFactory { + originalPath := path + + return func() (terraform.ResourceProvisioner, error) { + // First look for the provider on the PATH. + path, err := exec.LookPath(path) + if err != nil { + // If that doesn't work, look for it in the same directory + // as the executable that is running. + exePath, err := osext.Executable() + if err == nil { + path = filepath.Join( + filepath.Dir(exePath), + filepath.Base(originalPath)) + } + } + + // If we still don't have a path set, then set it to the + // original path and let any errors that happen bubble out. + if path == "" { + path = originalPath + } + + // Build the plugin client configuration and init the plugin + var config plugin.ClientConfig + config.Cmd = exec.Command(path) + config.Managed = true + client := plugin.NewClient(&config) + + // Request the RPC client and service name from the client + // so we can build the actual RPC-implemented provider. + rpcClient, err := client.Client() + if err != nil { + return nil, err + } + + service, err := client.Service() + if err != nil { + return nil, err + } + + return &rpc.ResourceProvisioner{ + Client: rpcClient, + Name: service, + }, nil + } +} diff --git a/main.go b/main.go index c47ecca55..49331e192 100644 --- a/main.go +++ b/main.go @@ -85,6 +85,7 @@ func wrappedMain() int { // Initialize the TFConfig settings for the commands... ContextOpts.Providers = config.ProviderFactories() + ContextOpts.Provisioners = config.ProvisionerFactories() // Get the command line args. We shortcut "--version" and "-v" to // just show the version. From 2423d135acb82c6100b354890f0fc35bddea71cb Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 9 Jul 2014 14:48:25 -0700 Subject: [PATCH 19/26] terraform: Move the config initialization of provisioners --- terraform/context.go | 8 -------- terraform/graph.go | 1 + 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/terraform/context.go b/terraform/context.go index 308ea9127..8b1b5c21d 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -814,14 +814,6 @@ func (c *Context) genericWalkFn(cb genericWalkFunc) depgraph.WalkFunc { } else { rn.Resource.Config = NewResourceConfig(rn.Config.RawConfig) } - - for _, prov := range rn.Resource.Provisioners { - if prov.RawConfig == nil { - prov.Config = new(ResourceConfig) - } else { - prov.Config = NewResourceConfig(prov.RawConfig) - } - } } else { rn.Resource.Config = nil } diff --git a/terraform/graph.go b/terraform/graph.go index d1fdf43c6..445d9e99d 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -772,6 +772,7 @@ func graphMapResourceProvisioners(g *depgraph.Graph, // Save the provisioner rn.Resource.Provisioners = append(rn.Resource.Provisioners, &ResourceProvisionerConfig{ Provisioner: provisioner, + Config: NewResourceConfig(p.RawConfig), RawConfig: p.RawConfig, }) } From abd59779884a2643deaae9fcb37916763519bde8 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 9 Jul 2014 15:01:47 -0700 Subject: [PATCH 20/26] Update config test to handle provisioners --- config_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/config_test.go b/config_test.go index a5203283c..fd29ddda1 100644 --- a/config_test.go +++ b/config_test.go @@ -33,6 +33,10 @@ func TestConfig_Merge(t *testing.T) { "foo": "bar", "bar": "blah", }, + Provisioners: map[string]string{ + "local": "local", + "remote": "bad", + }, } c2 := &Config{ @@ -40,6 +44,9 @@ func TestConfig_Merge(t *testing.T) { "bar": "baz", "baz": "what", }, + Provisioners: map[string]string{ + "remote": "remote", + }, } expected := &Config{ @@ -48,6 +55,10 @@ func TestConfig_Merge(t *testing.T) { "bar": "baz", "baz": "what", }, + Provisioners: map[string]string{ + "local": "local", + "remote": "remote", + }, } actual := c1.Merge(c2) From c8bc5658ab2d56563209eb7d7e2d7f532afc4a87 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 9 Jul 2014 15:02:00 -0700 Subject: [PATCH 21/26] terraform: Test that validate gets a config for provisioners --- terraform/context_test.go | 6 ++++++ terraform/resource_provisioner_mock.go | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/terraform/context_test.go b/terraform/context_test.go index 906bff7d9..793a9293d 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -161,6 +161,12 @@ func TestContextValidate_provisionerConfig_good(t *testing.T) { config := testConfig(t, "validate-bad-prov-conf") p := testProvider("aws") pr := testProvisioner() + pr.ValidateFn = func(c *ResourceConfig) ([]string, []error) { + if c == nil { + t.Fatalf("missing resource config for provisioner") + } + return nil, nil + } c := testContext(t, &ContextOpts{ Config: config, Providers: map[string]ResourceProviderFactory{ diff --git a/terraform/resource_provisioner_mock.go b/terraform/resource_provisioner_mock.go index e50342416..2f9a70b9f 100644 --- a/terraform/resource_provisioner_mock.go +++ b/terraform/resource_provisioner_mock.go @@ -15,6 +15,7 @@ type MockResourceProvisioner struct { ValidateCalled bool ValidateConfig *ResourceConfig + ValidateFn func(c *ResourceConfig) ([]string, []error) ValidateReturnWarns []string ValidateReturnErrors []error } @@ -22,6 +23,9 @@ type MockResourceProvisioner struct { func (p *MockResourceProvisioner) Validate(c *ResourceConfig) ([]string, []error) { p.ValidateCalled = true p.ValidateConfig = c + if p.ValidateFn != nil { + return p.ValidateFn(c) + } return p.ValidateReturnWarns, p.ValidateReturnErrors } From 83c1ed438f1994dead69850ff66829a2768f8716 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 9 Jul 2014 15:49:57 -0700 Subject: [PATCH 22/26] terraform: Fix and test provisioner configs --- terraform/context.go | 4 ++-- terraform/context_test.go | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/terraform/context.go b/terraform/context.go index 8b1b5c21d..e5d3763e6 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -532,12 +532,12 @@ func (c *Context) applyProvisioners(r *Resource, rs *ResourceState) (*ResourceSt for _, prov := range r.Provisioners { // Interpolate since we may have variables that depend on the // local resource. - if err := r.Config.interpolate(c); err != nil { + if err := prov.Config.interpolate(c); err != nil { return rs, err } // Invoke the Provisioner - rs, err = prov.Provisioner.Apply(rs, r.Config) + rs, err = prov.Provisioner.Apply(rs, prov.Config) if err != nil { return rs, err } diff --git a/terraform/context_test.go b/terraform/context_test.go index 793a9293d..314bf340a 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -352,6 +352,10 @@ func TestContextApply_Provisioner_compute(t *testing.T) { p.ApplyFn = testApplyFn p.DiffFn = testDiffFn pr.ApplyFn = func(rs *ResourceState, c *ResourceConfig) (*ResourceState, error) { + val, ok := c.Config["foo"] + if !ok || val != "computed_dynamical" { + t.Fatalf("bad value for foo: %v %#v", val, c) + } return rs, nil } ctx := testContext(t, &ContextOpts{ @@ -362,14 +366,15 @@ func TestContextApply_Provisioner_compute(t *testing.T) { Provisioners: map[string]ResourceProvisionerFactory{ "shell": testProvisionerFuncFixed(pr), }, + Variables: map[string]string{ + "value": "1", + }, }) 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) From 6ace8e12e5ba005c97fc90bb4d132ac99ce47a35 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 9 Jul 2014 15:50:10 -0700 Subject: [PATCH 23/26] provisioner/local-exec: Use interpolated values --- builtin/provisioners/local-exec/resource_provisioner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/provisioners/local-exec/resource_provisioner.go b/builtin/provisioners/local-exec/resource_provisioner.go index 7c8e01230..d8e157892 100644 --- a/builtin/provisioners/local-exec/resource_provisioner.go +++ b/builtin/provisioners/local-exec/resource_provisioner.go @@ -24,7 +24,7 @@ func (p *ResourceProvisioner) Apply( c *terraform.ResourceConfig) (*terraform.ResourceState, error) { // Get the command - commandRaw, ok := c.Get("command") + commandRaw, ok := c.Config["command"] if !ok { return s, fmt.Errorf("local-exec provisioner missing 'command'") } From 7721caf86787edbf068a74003fdc5a8bee374fc9 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 9 Jul 2014 16:36:46 -0700 Subject: [PATCH 24/26] provisioner/local-exec: Adding tests for Apply and Validate --- .../local-exec/resource_provisioner_test.go | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/builtin/provisioners/local-exec/resource_provisioner_test.go b/builtin/provisioners/local-exec/resource_provisioner_test.go index a066fc996..5f282872d 100644 --- a/builtin/provisioners/local-exec/resource_provisioner_test.go +++ b/builtin/provisioners/local-exec/resource_provisioner_test.go @@ -1,11 +1,74 @@ package localexec import ( + "io/ioutil" + "os" "testing" + "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/terraform" ) func TestResourceProvisioner_impl(t *testing.T) { var _ terraform.ResourceProvisioner = new(ResourceProvisioner) } + +func TestResourceProvider_Apply(t *testing.T) { + defer os.Remove("test_out") + c := testConfig(t, map[string]interface{}{ + "command": "echo foo > test_out", + }) + + p := new(ResourceProvisioner) + _, err := p.Apply(nil, c) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Check the file + raw, err := ioutil.ReadFile("test_out") + if err != nil { + t.Fatalf("err: %v", err) + } + + if string(raw) != "foo\n" { + t.Fatalf("bad: %s", raw) + } +} + +func TestResourceProvider_Validate_good(t *testing.T) { + c := testConfig(t, map[string]interface{}{ + "command": "echo foo", + }) + p := new(ResourceProvisioner) + warn, errs := p.Validate(c) + if len(warn) > 0 { + t.Fatalf("Warnings: %v", warn) + } + if len(errs) > 0 { + t.Fatalf("Errors: %v", errs) + } +} + +func TestResourceProvider_Validate_missing(t *testing.T) { + c := testConfig(t, map[string]interface{}{}) + p := new(ResourceProvisioner) + warn, errs := p.Validate(c) + if len(warn) > 0 { + t.Fatalf("Warnings: %v", warn) + } + if len(errs) == 0 { + t.Fatalf("Should have errors") + } +} + +func testConfig( + t *testing.T, + c map[string]interface{}) *terraform.ResourceConfig { + r, err := config.NewRawConfig(c) + if err != nil { + t.Fatalf("bad: %s", err) + } + + return terraform.NewResourceConfig(r) +} From a79e222ae3e0430de839a9839d051886ca655367 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 9 Jul 2014 16:54:34 -0700 Subject: [PATCH 25/26] rpc: Cleanup imports --- rpc/resource_provisioner.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rpc/resource_provisioner.go b/rpc/resource_provisioner.go index dc8a0ac16..82552cf4f 100644 --- a/rpc/resource_provisioner.go +++ b/rpc/resource_provisioner.go @@ -1,8 +1,9 @@ package rpc import ( - "github.com/hashicorp/terraform/terraform" "net/rpc" + + "github.com/hashicorp/terraform/terraform" ) // ResourceProvisioner is an implementation of terraform.ResourceProvisioner From 3e608ee8b9c017a736277df188ae878af4883b61 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Thu, 10 Jul 2014 12:01:26 -0700 Subject: [PATCH 26/26] terraform: Do not persist sensitive state --- terraform/state.go | 35 ++++++++++++++++++++++++++++++++++- terraform/state_test.go | 19 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/terraform/state.go b/terraform/state.go index 73069f847..49dca37f1 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -131,6 +131,21 @@ func (s *State) String() string { return buf.String() } +// sensitiveState is used to store sensitive state information +// that should not be serialized. This is only used temporarily +// and is restored into the state. +type sensitiveState struct { + ConnInfo map[string]*ResourceConnectionInfo + + once sync.Once +} + +func (s *sensitiveState) init() { + s.once.Do(func() { + s.ConnInfo = make(map[string]*ResourceConnectionInfo) + }) +} + // The format byte is prefixed into the state file format so that we have // the ability in the future to change the file format if we want for any // reason. @@ -172,7 +187,25 @@ func WriteState(d *State, dst io.Writer) error { return errors.New("failed to write state version byte") } - return gob.NewEncoder(dst).Encode(d) + // Prevent sensitive information from being serialized + sensitive := &sensitiveState{} + sensitive.init() + for name, r := range d.Resources { + if r.ConnInfo != nil { + sensitive.ConnInfo[name] = r.ConnInfo + r.ConnInfo = nil + } + } + + // Serialize the state + err = gob.NewEncoder(dst).Encode(d) + + // Restore the state + for name, info := range sensitive.ConnInfo { + d.Resources[name].ConnInfo = info + } + + return err } // ResourceConnectionInfo holds addresses, credentials and configuration diff --git a/terraform/state_test.go b/terraform/state_test.go index 8b4944387..53af97908 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -98,20 +98,39 @@ func TestReadWriteState(t *testing.T) { Resources: map[string]*ResourceState{ "foo": &ResourceState{ ID: "bar", + ConnInfo: &ResourceConnectionInfo{ + Type: "ssh", + Raw: map[string]string{ + "user": "root", + "password": "supersecret", + }, + }, }, }, } + // Checksum before the write + chksum := checksumStruct(t, state) + buf := new(bytes.Buffer) if err := WriteState(state, buf); err != nil { t.Fatalf("err: %s", err) } + // Checksum after the write + chksumAfter := checksumStruct(t, state) + if chksumAfter != chksum { + t.Fatalf("structure changed during serialization!") + } + actual, err := ReadState(buf) if err != nil { t.Fatalf("err: %s", err) } + // ReadState should not restore sensitive information! + state.Resources["foo"].ConnInfo = nil + if !reflect.DeepEqual(actual, state) { t.Fatalf("bad: %#v", actual) }