Merge pull request #26 from hashicorp/f-override

Configuration Overrides
This commit is contained in:
Mitchell Hashimoto 2014-07-21 05:33:37 -07:00
commit 3b6ef5d3ac
20 changed files with 850 additions and 117 deletions

58
config/append.go Normal file
View File

@ -0,0 +1,58 @@
package config
// Append appends one configuration to another.
//
// Append assumes that both configurations will not have
// conflicting variables, resources, etc. If they do, the
// problems will be caught in the validation phase.
//
// It is possible that c1, c2 on their own are not valid. For
// example, a resource in c2 may reference a variable in c1. But
// together, they would be valid.
func Append(c1, c2 *Config) (*Config, error) {
c := new(Config)
// Append unknown keys, but keep them unique since it is a set
unknowns := make(map[string]struct{})
for _, k := range c1.unknownKeys {
unknowns[k] = struct{}{}
}
for _, k := range c2.unknownKeys {
unknowns[k] = struct{}{}
}
for k, _ := range unknowns {
c.unknownKeys = append(c.unknownKeys, k)
}
if len(c1.Outputs) > 0 || len(c2.Outputs) > 0 {
c.Outputs = make(
[]*Output, 0, len(c1.Outputs)+len(c2.Outputs))
c.Outputs = append(c.Outputs, c1.Outputs...)
c.Outputs = append(c.Outputs, c2.Outputs...)
}
if len(c1.ProviderConfigs) > 0 || len(c2.ProviderConfigs) > 0 {
c.ProviderConfigs = make(
[]*ProviderConfig,
0, len(c1.ProviderConfigs)+len(c2.ProviderConfigs))
c.ProviderConfigs = append(c.ProviderConfigs, c1.ProviderConfigs...)
c.ProviderConfigs = append(c.ProviderConfigs, c2.ProviderConfigs...)
}
if len(c1.Resources) > 0 || len(c2.Resources) > 0 {
c.Resources = make(
[]*Resource,
0, len(c1.Resources)+len(c2.Resources))
c.Resources = append(c.Resources, c1.Resources...)
c.Resources = append(c.Resources, c2.Resources...)
}
if len(c1.Variables) > 0 || len(c2.Variables) > 0 {
c.Variables = make(
[]*Variable, 0, len(c1.Variables)+len(c2.Variables))
c.Variables = append(c.Variables, c1.Variables...)
c.Variables = append(c.Variables, c2.Variables...)
}
return c, nil
}

83
config/append_test.go Normal file
View File

@ -0,0 +1,83 @@
package config
import (
"reflect"
"testing"
)
func TestAppend(t *testing.T) {
cases := []struct {
c1, c2, result *Config
err bool
}{
{
&Config{
Outputs: []*Output{
&Output{Name: "foo"},
},
ProviderConfigs: []*ProviderConfig{
&ProviderConfig{Name: "foo"},
},
Resources: []*Resource{
&Resource{Name: "foo"},
},
Variables: []*Variable{
&Variable{Name: "foo"},
},
unknownKeys: []string{"foo"},
},
&Config{
Outputs: []*Output{
&Output{Name: "bar"},
},
ProviderConfigs: []*ProviderConfig{
&ProviderConfig{Name: "bar"},
},
Resources: []*Resource{
&Resource{Name: "bar"},
},
Variables: []*Variable{
&Variable{Name: "bar"},
},
unknownKeys: []string{"bar"},
},
&Config{
Outputs: []*Output{
&Output{Name: "foo"},
&Output{Name: "bar"},
},
ProviderConfigs: []*ProviderConfig{
&ProviderConfig{Name: "foo"},
&ProviderConfig{Name: "bar"},
},
Resources: []*Resource{
&Resource{Name: "foo"},
&Resource{Name: "bar"},
},
Variables: []*Variable{
&Variable{Name: "foo"},
&Variable{Name: "bar"},
},
unknownKeys: []string{"foo", "bar"},
},
false,
},
}
for i, tc := range cases {
actual, err := Append(tc.c1, tc.c2)
if (err != nil) != tc.err {
t.Fatalf("%d: error fail", i)
}
if !reflect.DeepEqual(actual, tc.result) {
t.Fatalf("%d: bad:\n\n%#v", i, actual)
}
}
}

View File

@ -13,10 +13,10 @@ import (
// Config is the configuration that comes from loading a collection
// of Terraform templates.
type Config struct {
ProviderConfigs map[string]*ProviderConfig
ProviderConfigs []*ProviderConfig
Resources []*Resource
Variables map[string]*Variable
Outputs map[string]*Output
Variables []*Variable
Outputs []*Output
// The fields below can be filled in by loaders for validation
// purposes.
@ -28,6 +28,7 @@ type Config struct {
// For example, Terraform needs to set the AWS access keys for the AWS
// resource provider.
type ProviderConfig struct {
Name string
RawConfig *RawConfig
}
@ -51,6 +52,7 @@ type Provisioner struct {
// Variable is a variable defined within the configuration.
type Variable struct {
Name string
Default string
Description string
defaultSet bool
@ -98,9 +100,10 @@ type UserVariable struct {
// ProviderConfigName returns the name of the provider configuration in
// the given mapping that maps to the proper provider configuration
// for this resource.
func ProviderConfigName(t string, pcs map[string]*ProviderConfig) string {
func ProviderConfigName(t string, pcs []*ProviderConfig) string {
lk := ""
for k, _ := range pcs {
for _, v := range pcs {
k := v.Name
if strings.HasPrefix(t, k) && len(k) > len(lk) {
lk = k
}
@ -124,6 +127,10 @@ func (c *Config) Validate() error {
}
vars := c.allVariables()
varMap := make(map[string]*Variable)
for _, v := range c.Variables {
varMap[v.Name] = v
}
// Check for references to user variables that do not actually
// exist and record those errors.
@ -134,7 +141,7 @@ func (c *Config) Validate() error {
continue
}
if _, ok := c.Variables[uv.Name]; !ok {
if _, ok := varMap[uv.Name]; !ok {
errs = append(errs, fmt.Errorf(
"%s: unknown variable referenced: %s",
source,
@ -244,6 +251,84 @@ func (c *Config) allVariables() map[string][]InterpolatedVariable {
return result
}
func (o *Output) mergerName() string {
return o.Name
}
func (o *Output) mergerMerge(m merger) merger {
o2 := m.(*Output)
result := *o
result.Name = o2.Name
result.RawConfig = result.RawConfig.merge(o2.RawConfig)
return &result
}
func (c *ProviderConfig) mergerName() string {
return c.Name
}
func (c *ProviderConfig) mergerMerge(m merger) merger {
c2 := m.(*ProviderConfig)
result := *c
result.Name = c2.Name
result.RawConfig = result.RawConfig.merge(c2.RawConfig)
return &result
}
func (r *Resource) mergerName() string {
return fmt.Sprintf("%s.%s", r.Type, r.Name)
}
func (r *Resource) mergerMerge(m merger) merger {
r2 := m.(*Resource)
result := *r
result.Name = r2.Name
result.Type = r2.Type
result.RawConfig = result.RawConfig.merge(r2.RawConfig)
if r2.Count > 0 {
result.Count = r2.Count
}
if len(r2.Provisioners) > 0 {
result.Provisioners = r2.Provisioners
}
return &result
}
// Merge merges two variables to create a new third variable.
func (v *Variable) Merge(v2 *Variable) *Variable {
// Shallow copy the variable
result := *v
// The names should be the same, but the second name always wins.
result.Name = v2.Name
if v2.defaultSet {
result.Default = v2.Default
result.defaultSet = true
}
if v2.Description != "" {
result.Description = v2.Description
}
return &result
}
func (v *Variable) mergerName() string {
return v.Name
}
func (v *Variable) mergerMerge(m merger) merger {
return v.Merge(m.(*Variable))
}
// Required tests whether a variable is required or not.
func (v *Variable) Required() bool {
return !v.defaultSet

View File

@ -151,11 +151,11 @@ func TestNewUserVariable(t *testing.T) {
}
func TestProviderConfigName(t *testing.T) {
pcs := map[string]*ProviderConfig{
"aw": new(ProviderConfig),
"aws": new(ProviderConfig),
"a": new(ProviderConfig),
"gce_": new(ProviderConfig),
pcs := []*ProviderConfig{
&ProviderConfig{Name: "aw"},
&ProviderConfig{Name: "aws"},
&ProviderConfig{Name: "a"},
&ProviderConfig{Name: "gce_"},
}
n := ProviderConfigName("aws_instance", pcs)

View File

@ -3,7 +3,6 @@ package config
import (
"fmt"
"io"
"strings"
)
// configurable is an interface that must be implemented by any configuration
@ -33,11 +32,15 @@ type fileLoaderFunc func(path string) (configurable, []string, error)
// executes the proper fileLoaderFunc.
func loadTree(root string) (*importTree, error) {
var f fileLoaderFunc
if strings.HasSuffix(root, ".tf") {
switch ext(root) {
case ".tf":
fallthrough
case ".tf.json":
f = loadFileLibucl
} else if strings.HasSuffix(root, ".tf.json") {
f = loadFileLibucl
} else {
default:
}
if f == nil {
return nil, fmt.Errorf(
"%s: unknown configuration format. Use '.tf' or '.tf.json' extension",
root)

View File

@ -2,7 +2,11 @@ package config
import (
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
)
// Load loads the Terraform configuration from a given file.
@ -29,28 +33,82 @@ func Load(path string) (*Config, error) {
}
// LoadDir loads all the Terraform configuration files in a single
// directory and merges them together.
func LoadDir(path string) (*Config, error) {
matches, err := filepath.Glob(filepath.Join(path, "*.tf"))
// directory and appends them together.
//
// Special files known as "override files" can also be present, which
// are merged into the loaded configuration. That is, the non-override
// files are loaded first to create the configuration. Then, the overrides
// are merged into the configuration to create the final configuration.
//
// Files are loaded in lexical order.
func LoadDir(root string) (*Config, error) {
var files, overrides []string
f, err := os.Open(root)
if err != nil {
return nil, err
}
if len(matches) == 0 {
err = nil
for err != io.EOF {
var fis []os.FileInfo
fis, err = f.Readdir(128)
if err != nil && err != io.EOF {
f.Close()
return nil, err
}
for _, fi := range fis {
// Ignore directories
if fi.IsDir() {
continue
}
// Only care about files that are valid to load
name := fi.Name()
extValue := ext(name)
if extValue == "" {
continue
}
// Determine if we're dealing with an override
nameNoExt := name[:len(name)-len(extValue)]
override := nameNoExt == "override" ||
strings.HasSuffix(nameNoExt, "_override")
path := filepath.Join(root, name)
if override {
overrides = append(overrides, path)
} else {
files = append(files, path)
}
}
}
// Close the directory, we're done with it
f.Close()
if len(files) == 0 {
return nil, fmt.Errorf(
"No Terraform configuration files found in directory: %s",
path)
root)
}
var result *Config
for _, f := range matches {
// Sort the files and overrides so we have a deterministic order
sort.Strings(files)
sort.Strings(overrides)
// Load all the regular files, append them to each other.
for _, f := range files {
c, err := Load(f)
if err != nil {
return nil, err
}
if result != nil {
result, err = Merge(result, c)
result, err = Append(result, c)
if err != nil {
return nil, err
}
@ -59,5 +117,30 @@ func LoadDir(path string) (*Config, error) {
}
}
// Load all the overrides, and merge them into the config
for _, f := range overrides {
c, err := Load(f)
if err != nil {
return nil, err
}
result, err = Merge(result, c)
if err != nil {
return nil, err
}
}
return result, nil
}
// Ext returns the Terraform configuration extension of the given
// path, or a blank string if it is an invalid function.
func ext(path string) string {
if strings.HasSuffix(path, ".tf") {
return ".tf"
} else if strings.HasSuffix(path, ".tf.json") {
return ".tf.json"
} else {
return ""
}
}

View File

@ -45,8 +45,11 @@ func (t *libuclConfigurable) Config() (*Config, error) {
// Start building up the actual configuration. We start with
// variables.
// TODO(mitchellh): Make function like loadVariablesLibucl so that
// duplicates aren't overriden
config := new(Config)
config.Variables = make(map[string]*Variable)
if len(rawConfig.Variable) > 0 {
config.Variables = make([]*Variable, 0, len(rawConfig.Variable))
for k, v := range rawConfig.Variable {
defaultSet := false
for _, f := range v.Fields {
@ -56,10 +59,12 @@ func (t *libuclConfigurable) Config() (*Config, error) {
}
}
config.Variables[k] = &Variable{
config.Variables = append(config.Variables, &Variable{
Name: k,
Default: v.Default,
Description: v.Description,
defaultSet: defaultSet,
})
}
}
@ -178,7 +183,7 @@ func loadFileLibucl(root string) (configurable, []string, error) {
// LoadOutputsLibucl recurses into the given libucl object and turns
// it into a mapping of outputs.
func loadOutputsLibucl(o *libucl.Object) (map[string]*Output, error) {
func loadOutputsLibucl(o *libucl.Object) ([]*Output, error) {
objects := make(map[string]*libucl.Object)
// Iterate over all the "output" blocks and get the keys along with
@ -196,8 +201,13 @@ func loadOutputsLibucl(o *libucl.Object) (map[string]*Output, error) {
}
iter.Close()
// If we have none, just return nil
if len(objects) == 0 {
return nil, nil
}
// Go through each object and turn it into an actual result.
result := make(map[string]*Output)
result := make([]*Output, 0, len(objects))
for n, o := range objects {
var config map[string]interface{}
@ -213,10 +223,10 @@ func loadOutputsLibucl(o *libucl.Object) (map[string]*Output, error) {
err)
}
result[n] = &Output{
result = append(result, &Output{
Name: n,
RawConfig: rawConfig,
}
})
}
return result, nil
@ -224,7 +234,7 @@ func loadOutputsLibucl(o *libucl.Object) (map[string]*Output, error) {
// LoadProvidersLibucl recurses into the given libucl object and turns
// it into a mapping of provider configs.
func loadProvidersLibucl(o *libucl.Object) (map[string]*ProviderConfig, error) {
func loadProvidersLibucl(o *libucl.Object) ([]*ProviderConfig, error) {
objects := make(map[string]*libucl.Object)
// Iterate over all the "provider" blocks and get the keys along with
@ -242,8 +252,12 @@ func loadProvidersLibucl(o *libucl.Object) (map[string]*ProviderConfig, error) {
}
iter.Close()
if len(objects) == 0 {
return nil, nil
}
// Go through each object and turn it into an actual result.
result := make(map[string]*ProviderConfig)
result := make([]*ProviderConfig, 0, len(objects))
for n, o := range objects {
var config map[string]interface{}
@ -259,9 +273,10 @@ func loadProvidersLibucl(o *libucl.Object) (map[string]*ProviderConfig, error) {
err)
}
result[n] = &ProviderConfig{
result = append(result, &ProviderConfig{
Name: n,
RawConfig: rawConfig,
}
})
}
return result, nil

View File

@ -116,16 +116,6 @@ func TestLoad_variables(t *testing.T) {
if actual != strings.TrimSpace(variablesVariablesStr) {
t.Fatalf("bad:\n%s", actual)
}
if !c.Variables["foo"].Required() {
t.Fatal("foo should be required")
}
if c.Variables["bar"].Required() {
t.Fatal("bar should not be required")
}
if c.Variables["baz"].Required() {
t.Fatal("baz should not be required")
}
}
func TestLoadDir_basic(t *testing.T) {
@ -166,16 +156,64 @@ func TestLoadDir_noConfigs(t *testing.T) {
}
}
func outputsStr(os map[string]*Output) string {
func TestLoadDir_noMerge(t *testing.T) {
c, err := LoadDir(filepath.Join(fixtureDir, "dir-merge"))
if err != nil {
t.Fatalf("err: %s", err)
}
if c == nil {
t.Fatal("config should not be nil")
}
if err := c.Validate(); err == nil {
t.Fatal("should not be valid")
}
}
func TestLoadDir_override(t *testing.T) {
c, err := LoadDir(filepath.Join(fixtureDir, "dir-override"))
if err != nil {
t.Fatalf("err: %s", err)
}
if c == nil {
t.Fatal("config should not be nil")
}
actual := variablesStr(c.Variables)
if actual != strings.TrimSpace(dirOverrideVariablesStr) {
t.Fatalf("bad:\n%s", actual)
}
actual = providerConfigsStr(c.ProviderConfigs)
if actual != strings.TrimSpace(dirOverrideProvidersStr) {
t.Fatalf("bad:\n%s", actual)
}
actual = resourcesStr(c.Resources)
if actual != strings.TrimSpace(dirOverrideResourcesStr) {
t.Fatalf("bad:\n%s", actual)
}
actual = outputsStr(c.Outputs)
if actual != strings.TrimSpace(dirOverrideOutputsStr) {
t.Fatalf("bad:\n%s", actual)
}
}
func outputsStr(os []*Output) string {
ns := make([]string, 0, len(os))
for n, _ := range os {
ns = append(ns, n)
m := make(map[string]*Output)
for _, o := range os {
ns = append(ns, o.Name)
m[o.Name] = o
}
sort.Strings(ns)
result := ""
for _, n := range ns {
o := os[n]
o := m[n]
result += fmt.Sprintf("%s\n", n)
@ -256,17 +294,19 @@ func TestLoad_connections(t *testing.T) {
// This helper turns a provider configs field into a deterministic
// string value for comparison in tests.
func providerConfigsStr(pcs map[string]*ProviderConfig) string {
func providerConfigsStr(pcs []*ProviderConfig) string {
result := ""
ns := make([]string, 0, len(pcs))
for n, _ := range pcs {
ns = append(ns, n)
m := make(map[string]*ProviderConfig)
for _, n := range pcs {
ns = append(ns, n.Name)
m[n.Name] = n
}
sort.Strings(ns)
for _, n := range ns {
pc := pcs[n]
pc := m[n]
result += fmt.Sprintf("%s\n", n)
@ -384,16 +424,18 @@ func resourcesStr(rs []*Resource) string {
// This helper turns a variables field into a deterministic
// string value for comparison in tests.
func variablesStr(vs map[string]*Variable) string {
func variablesStr(vs []*Variable) string {
result := ""
ks := make([]string, 0, len(vs))
for k, _ := range vs {
ks = append(ks, k)
m := make(map[string]*Variable)
for _, v := range vs {
ks = append(ks, v.Name)
m[v.Name] = v
}
sort.Strings(ks)
for _, k := range ks {
v := vs[k]
v := m[k]
if v.Default == "" {
v.Default = "<>"
@ -402,9 +444,15 @@ func variablesStr(vs map[string]*Variable) string {
v.Description = "<>"
}
required := ""
if v.Required() {
required = " (required)"
}
result += fmt.Sprintf(
"%s\n %s\n %s\n",
"%s%s\n %s\n %s\n",
k,
required,
v.Default,
v.Description)
}
@ -486,8 +534,46 @@ foo
bar
`
const dirOverrideOutputsStr = `
web_ip
vars
resource: aws_instance.web.private_ip
`
const dirOverrideProvidersStr = `
aws
access_key
secret_key
do
api_key
vars
user: var.foo
`
const dirOverrideResourcesStr = `
aws_instance[db] (x1)
ami
security_groups
aws_instance[web] (x1)
ami
foo
network_interface
security_groups
vars
resource: aws_security_group.firewall.foo
user: var.foo
aws_security_group[firewall] (x5)
`
const dirOverrideVariablesStr = `
foo
bar
bar
`
const importProvidersStr = `
aws
bar
foo
`
@ -497,7 +583,7 @@ aws_security_group[web] (x1)
`
const importVariablesStr = `
bar
bar (required)
<>
<>
foo
@ -538,7 +624,7 @@ bar
baz
foo
<>
foo
foo (required)
<>
<>
`

View File

@ -1,9 +1,5 @@
package config
import (
"fmt"
)
// Merge merges two configurations into a single configuration.
//
// Merge allows for the two configurations to have duplicate resources,
@ -24,59 +20,135 @@ func Merge(c1, c2 *Config) (*Config, error) {
c.unknownKeys = append(c.unknownKeys, k)
}
// Merge variables: Variable merging is quite simple. Set fields in
// later set variables override those earlier.
c.Variables = c1.Variables
for k, v2 := range c2.Variables {
v1, ok := c.Variables[k]
if ok {
if v2.Default == "" {
v2.Default = v1.Default
// NOTE: Everything below is pretty gross. Due to the lack of generics
// in Go, there is some hoop-jumping involved to make this merging a
// little more test-friendly and less repetitive. Ironically, making it
// less repetitive involves being a little repetitive, but I prefer to
// be repetitive with things that are less error prone than things that
// are more error prone (more logic). Type conversions to an interface
// are pretty low-error.
var m1, m2, mresult []merger
// Outputs
m1 = make([]merger, 0, len(c1.Outputs))
m2 = make([]merger, 0, len(c2.Outputs))
for _, v := range c1.Outputs {
m1 = append(m1, v)
}
if v2.Description == "" {
v2.Description = v1.Description
for _, v := range c2.Outputs {
m2 = append(m2, v)
}
mresult = mergeSlice(m1, m2)
if len(mresult) > 0 {
c.Outputs = make([]*Output, len(mresult))
for i, v := range mresult {
c.Outputs[i] = v.(*Output)
}
}
c.Variables[k] = v2
// Provider Configs
m1 = make([]merger, 0, len(c1.ProviderConfigs))
m2 = make([]merger, 0, len(c2.ProviderConfigs))
for _, v := range c1.ProviderConfigs {
m1 = append(m1, v)
}
for _, v := range c2.ProviderConfigs {
m2 = append(m2, v)
}
mresult = mergeSlice(m1, m2)
if len(mresult) > 0 {
c.ProviderConfigs = make([]*ProviderConfig, len(mresult))
for i, v := range mresult {
c.ProviderConfigs[i] = v.(*ProviderConfig)
}
}
// Merge outputs: If they collide, just take the latest one for now. In
// the future, we might provide smarter merge functionality.
c.Outputs = make(map[string]*Output)
for k, v := range c1.Outputs {
c.Outputs[k] = v
// Resources
m1 = make([]merger, 0, len(c1.Resources))
m2 = make([]merger, 0, len(c2.Resources))
for _, v := range c1.Resources {
m1 = append(m1, v)
}
for _, v := range c2.Resources {
m2 = append(m2, v)
}
mresult = mergeSlice(m1, m2)
if len(mresult) > 0 {
c.Resources = make([]*Resource, len(mresult))
for i, v := range mresult {
c.Resources[i] = v.(*Resource)
}
for k, v := range c2.Outputs {
c.Outputs[k] = v
}
// Merge provider configs: If they collide, we just take the latest one
// for now. In the future, we might provide smarter merge functionality.
c.ProviderConfigs = make(map[string]*ProviderConfig)
for k, v := range c1.ProviderConfigs {
c.ProviderConfigs[k] = v
// Variables
m1 = make([]merger, 0, len(c1.Variables))
m2 = make([]merger, 0, len(c2.Variables))
for _, v := range c1.Variables {
m1 = append(m1, v)
}
for k, v := range c2.ProviderConfigs {
c.ProviderConfigs[k] = v
for _, v := range c2.Variables {
m2 = append(m2, v)
}
// Merge resources: If they collide, we just take the latest one
// for now. In the future, we might provide smarter merge functionality.
resources := make(map[string]*Resource)
for _, r := range c1.Resources {
id := fmt.Sprintf("%s[%s]", r.Type, r.Name)
resources[id] = r
mresult = mergeSlice(m1, m2)
if len(mresult) > 0 {
c.Variables = make([]*Variable, len(mresult))
for i, v := range mresult {
c.Variables[i] = v.(*Variable)
}
for _, r := range c2.Resources {
id := fmt.Sprintf("%s[%s]", r.Type, r.Name)
resources[id] = r
}
c.Resources = make([]*Resource, 0, len(resources))
for _, r := range resources {
c.Resources = append(c.Resources, r)
}
return c, nil
}
// merger is an interface that must be implemented by types that are
// merge-able. This simplifies the implementation of Merge for the various
// components of a Config.
type merger interface {
mergerName() string
mergerMerge(merger) merger
}
// mergeSlice merges a slice of mergers.
func mergeSlice(m1, m2 []merger) []merger {
r := make([]merger, len(m1), len(m1)+len(m2))
copy(r, m1)
m := map[string]struct{}{}
for _, v2 := range m2 {
// If we already saw it, just append it because its a
// duplicate and invalid...
name := v2.mergerName()
if _, ok := m[name]; ok {
r = append(r, v2)
continue
}
m[name] = struct{}{}
// Find an original to override
var original merger
originalIndex := -1
for i, v := range m1 {
if v.mergerName() == name {
originalIndex = i
original = v
break
}
}
var v merger
if original == nil {
v = v2
} else {
v = original.mergerMerge(v2)
}
if originalIndex == -1 {
r = append(r, v)
} else {
r[originalIndex] = v
}
}
return r
}

149
config/merge_test.go Normal file
View File

@ -0,0 +1,149 @@
package config
import (
"reflect"
"testing"
)
func TestMerge(t *testing.T) {
cases := []struct {
c1, c2, result *Config
err bool
}{
// Normal good case.
{
&Config{
Outputs: []*Output{
&Output{Name: "foo"},
},
ProviderConfigs: []*ProviderConfig{
&ProviderConfig{Name: "foo"},
},
Resources: []*Resource{
&Resource{Name: "foo"},
},
Variables: []*Variable{
&Variable{Name: "foo"},
},
unknownKeys: []string{"foo"},
},
&Config{
Outputs: []*Output{
&Output{Name: "bar"},
},
ProviderConfigs: []*ProviderConfig{
&ProviderConfig{Name: "bar"},
},
Resources: []*Resource{
&Resource{Name: "bar"},
},
Variables: []*Variable{
&Variable{Name: "bar"},
},
unknownKeys: []string{"bar"},
},
&Config{
Outputs: []*Output{
&Output{Name: "foo"},
&Output{Name: "bar"},
},
ProviderConfigs: []*ProviderConfig{
&ProviderConfig{Name: "foo"},
&ProviderConfig{Name: "bar"},
},
Resources: []*Resource{
&Resource{Name: "foo"},
&Resource{Name: "bar"},
},
Variables: []*Variable{
&Variable{Name: "foo"},
&Variable{Name: "bar"},
},
unknownKeys: []string{"foo", "bar"},
},
false,
},
// Test that when merging duplicates, it merges into the
// first, but keeps the duplicates so that errors still
// happen.
{
&Config{
Outputs: []*Output{
&Output{Name: "foo"},
},
ProviderConfigs: []*ProviderConfig{
&ProviderConfig{Name: "foo"},
},
Resources: []*Resource{
&Resource{Name: "foo"},
},
Variables: []*Variable{
&Variable{Name: "foo", Default: "foo"},
&Variable{Name: "foo"},
},
unknownKeys: []string{"foo"},
},
&Config{
Outputs: []*Output{
&Output{Name: "bar"},
},
ProviderConfigs: []*ProviderConfig{
&ProviderConfig{Name: "bar"},
},
Resources: []*Resource{
&Resource{Name: "bar"},
},
Variables: []*Variable{
&Variable{Name: "foo", Default: "bar", defaultSet: true},
&Variable{Name: "bar"},
},
unknownKeys: []string{"bar"},
},
&Config{
Outputs: []*Output{
&Output{Name: "foo"},
&Output{Name: "bar"},
},
ProviderConfigs: []*ProviderConfig{
&ProviderConfig{Name: "foo"},
&ProviderConfig{Name: "bar"},
},
Resources: []*Resource{
&Resource{Name: "foo"},
&Resource{Name: "bar"},
},
Variables: []*Variable{
&Variable{Name: "foo", Default: "bar", defaultSet: true},
&Variable{Name: "foo"},
&Variable{Name: "bar"},
},
unknownKeys: []string{"foo", "bar"},
},
false,
},
}
for i, tc := range cases {
actual, err := Merge(tc.c1, tc.c2)
if (err != nil) != tc.err {
t.Fatalf("%d: error fail", i)
}
if !reflect.DeepEqual(actual, tc.result) {
t.Fatalf("%d: bad:\n\n%#v", i, actual)
}
}
}

View File

@ -92,6 +92,25 @@ func (r *RawConfig) init() error {
return nil
}
func (r *RawConfig) merge(r2 *RawConfig) *RawConfig {
rawRaw, err := copystructure.Copy(r.Raw)
if err != nil {
panic(err)
}
raw := rawRaw.(map[string]interface{})
for k, v := range r2.Raw {
raw[k] = v
}
result, err := NewRawConfig(raw)
if err != nil {
panic(err)
}
return result
}
// UnknownKeys returns the keys of the configuration that are unknown
// because they had interpolated variables that must be computed.
func (r *RawConfig) UnknownKeys() []string {

View File

@ -0,0 +1,8 @@
variable "foo" {
default = "bar";
description = "bar";
}
resource "aws_instance" "db" {
security_groups = "${aws_security_group.firewall.*.id}"
}

View File

@ -0,0 +1,2 @@
resource "aws_instance" "db" {
}

View File

@ -0,0 +1,9 @@
{
"resource": {
"aws_instance": {
"web": {
"foo": "bar",
}
}
}
}

View File

@ -0,0 +1,17 @@
variable "foo" {
default = "bar";
description = "bar";
}
provider "aws" {
access_key = "foo";
secret_key = "bar";
}
resource "aws_instance" "db" {
security_groups = "${aws_security_group.firewall.*.id}"
}
output "web_ip" {
value = "${aws_instance.web.private_ip}"
}

View File

@ -0,0 +1,10 @@
{
"resource": {
"aws_instance": {
"db": {
"ami": "foo",
"security_groups": ""
}
}
}
}

View File

@ -0,0 +1,20 @@
provider "do" {
api_key = "${var.foo}";
}
resource "aws_security_group" "firewall" {
count = 5
}
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

@ -462,7 +462,8 @@ func graphAddProviderConfigs(g *depgraph.Graph, c *config.Config) {
}
// Look up the provider config for this resource
pcName := config.ProviderConfigName(resourceNode.Type, c.ProviderConfigs)
pcName := config.ProviderConfigName(
resourceNode.Type, c.ProviderConfigs)
if pcName == "" {
continue
}
@ -470,11 +471,22 @@ func graphAddProviderConfigs(g *depgraph.Graph, c *config.Config) {
// We have one, so build the noun if it hasn't already been made
pcNoun, ok := pcNouns[pcName]
if !ok {
var pc *config.ProviderConfig
for _, v := range c.ProviderConfigs {
if v.Name == pcName {
pc = v
break
}
}
if pc == nil {
panic("pc not found")
}
pcNoun = &depgraph.Noun{
Name: fmt.Sprintf("provider.%s", pcName),
Meta: &GraphNodeResourceProvider{
ID: pcName,
Config: c.ProviderConfigs[pcName],
Config: pc,
},
}
pcNouns[pcName] = pcNoun

View File

@ -53,6 +53,8 @@ func TestReadWritePlan(t *testing.T) {
t.Fatalf("err: %s", err)
}
println(reflect.DeepEqual(actual.Config.Variables, plan.Config.Variables))
if !reflect.DeepEqual(actual, plan) {
t.Fatalf("bad: %#v", actual)
}

View File

@ -13,9 +13,9 @@ func smcUserVariables(c *config.Config, vs map[string]string) []error {
// Check that all required variables are present
required := make(map[string]struct{})
for k, v := range c.Variables {
for _, v := range c.Variables {
if v.Required() {
required[k] = struct{}{}
required[v.Name] = struct{}{}
}
}
for k, _ := range vs {