From 2791badf01981bbe80a41adab73ba2ee7805f8fc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Sep 2014 09:13:15 -0700 Subject: [PATCH] terraform: ask for input for providers --- terraform/context.go | 137 ++++++++---------- terraform/context_test.go | 39 +++++ terraform/graph.go | 47 +++++- terraform/resource.go | 13 +- terraform/resource_provider_mock.go | 4 + terraform/terraform_test.go | 13 ++ .../test-fixtures/input-provider/main.tf | 1 + 7 files changed, 173 insertions(+), 81 deletions(-) create mode 100644 terraform/test-fixtures/input-provider/main.tf diff --git a/terraform/context.go b/terraform/context.go index afeebfc68..ab3a008d8 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -25,14 +25,15 @@ type genericWalkFunc func(*walkContext, *Resource) error // // Additionally, a context can be created from a Plan using Plan.Context. type Context struct { - module *module.Tree - diff *Diff - hooks []Hook - state *State - providers map[string]ResourceProviderFactory - provisioners map[string]ResourceProvisionerFactory - variables map[string]string - uiInput UIInput + module *module.Tree + diff *Diff + hooks []Hook + state *State + providerConfig map[string]map[string]map[string]interface{} + providers map[string]ResourceProviderFactory + provisioners map[string]ResourceProvisionerFactory + variables map[string]string + uiInput UIInput l sync.Mutex // Lock acquired during any task parCh chan struct{} // Semaphore used to limit parallelism @@ -78,14 +79,15 @@ func NewContext(opts *ContextOpts) *Context { parCh := make(chan struct{}, par) return &Context{ - diff: opts.Diff, - hooks: hooks, - module: opts.Module, - state: opts.State, - providers: opts.Providers, - provisioners: opts.Provisioners, - variables: opts.Variables, - uiInput: opts.UIInput, + diff: opts.Diff, + hooks: hooks, + module: opts.Module, + state: opts.State, + providerConfig: make(map[string]map[string]map[string]interface{}), + providers: opts.Providers, + provisioners: opts.Provisioners, + variables: opts.Variables, + uiInput: opts.UIInput, parCh: parCh, sh: sh, @@ -548,31 +550,44 @@ func (c *walkContext) inputWalkFn() depgraph.WalkFunc { // Resources don't matter for input. Continue. return nil case *GraphNodeResourceProvider: - return nil - /* - // If we already did this provider, then we're done. - meta.Lock() - _, ok := meta.Done[rn.ID] - meta.Unlock() - if ok { - return nil - } + // Acquire the lock the whole time so we only ask for input + // one at a time. + meta.Lock() + defer meta.Unlock() - // Get the raw configuration because this is what we - // pass into the API. - var raw *config.RawConfig - sharedProvider := rn.Provider - if sharedProvider.Config != nil { - raw = sharedProvider.Config.RawConfig - } - rc := NewResourceConfig(raw) + // If we already did this provider, then we're done. + if _, ok := meta.Done[rn.ID]; ok { + return nil + } - // Go through each provider and capture the input necessary - // to satisfy it. - for k, p := range sharedProvider.Providers { - ws, es := p.Validate(rc) + // Get the raw configuration because this is what we + // pass into the API. + var raw *config.RawConfig + sharedProvider := rn.Provider + if sharedProvider.Config != nil { + raw = sharedProvider.Config.RawConfig + } + rc := NewResourceConfig(raw) + + // Go through each provider and capture the input necessary + // to satisfy it. + configs := make(map[string]map[string]interface{}) + for k, p := range sharedProvider.Providers { + newc, err := p.Input(c.Context.uiInput, rc) + if err != nil { + return fmt.Errorf( + "Error configuring %s: %s", k, err) } - */ + if newc != nil { + configs[k] = newc.Raw + } + } + + // Mark this provider as done + meta.Done[rn.ID] = struct{}{} + + // Set the configuration + c.Context.providerConfig[rn.ID] = configs } return nil @@ -1116,47 +1131,17 @@ func (c *walkContext) genericWalkFn(cb genericWalkFunc) depgraph.WalkFunc { case *GraphNodeResourceProvider: sharedProvider := m.Provider - // Interpolate in the variables and configure all the providers - var raw *config.RawConfig - if sharedProvider.Config != nil { - raw = sharedProvider.Config.RawConfig + // Check if we have an override + cs, ok := c.Context.providerConfig[m.ID] + if !ok { + cs = make(map[string]map[string]interface{}) } - // If we have a parent, then merge in the parent configurations - // properly so we "inherit" the configurations. - if sharedProvider.Parent != nil { - var rawMap map[string]interface{} - if raw != nil { - rawMap = raw.Raw - } - - parent := sharedProvider.Parent - for parent != nil { - if parent.Config != nil { - if rawMap == nil { - rawMap = parent.Config.RawConfig.Raw - } - - for k, v := range parent.Config.RawConfig.Config() { - rawMap[k] = v - } - } - - parent = parent.Parent - } - - // Update our configuration to be the merged result - var err error - raw, err = config.NewRawConfig(rawMap) - if err != nil { - return fmt.Errorf("Error merging configurations: %s", err) - } - } - - rc := NewResourceConfig(raw) - rc.interpolate(c) - for k, p := range sharedProvider.Providers { + // Merge the configurations to get what we use to configure with + rc := sharedProvider.MergeConfig(false, cs[k]) + rc.interpolate(c) + log.Printf("[INFO] Configuring provider: %s", k) err := p.Configure(rc) if err != nil { diff --git a/terraform/context_test.go b/terraform/context_test.go index 45064de3e..95ca3dd0f 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -460,6 +460,45 @@ func TestContextInput(t *testing.T) { } } +func TestContextInput_provider(t *testing.T) { + m := testModule(t, "input-provider") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + var actual interface{} + p.InputFn = func(i UIInput, c *ResourceConfig) (*ResourceConfig, error) { + c.Raw["foo"] = "bar" + return c, nil + } + p.ConfigureFn = func(c *ResourceConfig) error { + actual = c.Raw["foo"] + return nil + } + + if err := ctx.Input(); err != nil { + t.Fatalf("err: %s", err) + } + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + if _, err := ctx.Apply(); err != nil { + t.Fatalf("err: %s", err) + } + + if !reflect.DeepEqual(actual, "bar") { + t.Fatalf("bad: %#v", actual) + } +} + func TestContextApply(t *testing.T) { m := testModule(t, "apply-good") p := testProvider("aws") diff --git a/terraform/graph.go b/terraform/graph.go index dba16975b..279c08f26 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -119,7 +119,8 @@ type graphSharedProvider struct { ProviderKeys []string Parent *graphSharedProvider - parentNoun *depgraph.Noun + overrideConfig map[string]map[string]interface{} + parentNoun *depgraph.Noun } // Graph builds a dependency graph of all the resources for infrastructure @@ -1461,6 +1462,50 @@ func graphMapResourceProvisioners(g *depgraph.Graph, return nil } +// MergeConfig merges all the configurations in the proper order +// to result in the final configuration to use to configure this +// provider. +func (p *graphSharedProvider) MergeConfig( + raw bool, override map[string]interface{}) *ResourceConfig { + var rawMap map[string]interface{} + if override != nil { + rawMap = override + } else if p.Config != nil { + rawMap = p.Config.RawConfig.Raw + } + if rawMap == nil { + rawMap = make(map[string]interface{}) + } + + // Merge in all the parent configurations + if p.Parent != nil { + parent := p.Parent + for parent != nil { + if parent.Config != nil { + var merge map[string]interface{} + if raw { + merge = parent.Config.RawConfig.Raw + } else { + merge = parent.Config.RawConfig.Config() + } + + for k, v := range merge { + rawMap[k] = v + } + } + + parent = parent.Parent + } + } + + rc, err := config.NewRawConfig(rawMap) + if err != nil { + panic("error building config: " + err.Error()) + } + + return NewResourceConfig(rc) +} + // 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 f7e0855e7..0df27524a 100644 --- a/terraform/resource.go +++ b/terraform/resource.go @@ -199,11 +199,16 @@ func (c *ResourceConfig) interpolate(ctx *walkContext) error { } } - if c.raw != nil { - c.ComputedKeys = c.raw.UnknownKeys() - c.Raw = c.raw.Raw - c.Config = c.raw.Config() + if c.raw == nil { + var err error + c.raw, err = config.NewRawConfig(make(map[string]interface{})) + if err != nil { + return err + } } + c.ComputedKeys = c.raw.UnknownKeys() + c.Raw = c.raw.Raw + c.Config = c.raw.Config() return nil } diff --git a/terraform/resource_provider_mock.go b/terraform/resource_provider_mock.go index 29db7eeb3..6a1c29b6d 100644 --- a/terraform/resource_provider_mock.go +++ b/terraform/resource_provider_mock.go @@ -17,6 +17,7 @@ type MockResourceProvider struct { InputConfig *ResourceConfig InputReturnConfig *ResourceConfig InputReturnError error + InputFn func(UIInput, *ResourceConfig) (*ResourceConfig, error) ApplyCalled bool ApplyInfo *InstanceInfo ApplyState *InstanceState @@ -61,6 +62,9 @@ func (p *MockResourceProvider) Input( p.InputCalled = true p.InputInput = input p.InputConfig = c + if p.InputFn != nil { + return p.InputFn(input, c) + } return p.InputReturnConfig, p.InputReturnError } diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 6be8cdd4b..d01e98acb 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -113,6 +113,19 @@ func (h *HookRecordApplyOrder) PreApply( // Below are all the constant strings that are the expected output for // various tests. +const testTerraformInputProviderStr = ` +aws_instance.bar: + ID = foo + bar = override + foo = us-east-1 + type = aws_instance +aws_instance.foo: + ID = foo + bar = baz + num = 2 + type = aws_instance +` + const testTerraformInputVarsStr = ` aws_instance.bar: ID = foo diff --git a/terraform/test-fixtures/input-provider/main.tf b/terraform/test-fixtures/input-provider/main.tf new file mode 100644 index 000000000..919f140bb --- /dev/null +++ b/terraform/test-fixtures/input-provider/main.tf @@ -0,0 +1 @@ +resource "aws_instance" "foo" {}