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 // Config is the configuration that comes from loading a collection
// of Terraform templates. // of Terraform templates.
type Config struct { type Config struct {
ProviderConfigs map[string]*ProviderConfig ProviderConfigs []*ProviderConfig
Resources []*Resource Resources []*Resource
Variables map[string]*Variable Variables []*Variable
Outputs map[string]*Output Outputs []*Output
// The fields below can be filled in by loaders for validation // The fields below can be filled in by loaders for validation
// purposes. // purposes.
@ -28,6 +28,7 @@ type Config struct {
// For example, Terraform needs to set the AWS access keys for the AWS // For example, Terraform needs to set the AWS access keys for the AWS
// resource provider. // resource provider.
type ProviderConfig struct { type ProviderConfig struct {
Name string
RawConfig *RawConfig RawConfig *RawConfig
} }
@ -51,6 +52,7 @@ type Provisioner struct {
// Variable is a variable defined within the configuration. // Variable is a variable defined within the configuration.
type Variable struct { type Variable struct {
Name string
Default string Default string
Description string Description string
defaultSet bool defaultSet bool
@ -98,9 +100,10 @@ type UserVariable struct {
// ProviderConfigName returns the name of the provider configuration in // ProviderConfigName returns the name of the provider configuration in
// the given mapping that maps to the proper provider configuration // the given mapping that maps to the proper provider configuration
// for this resource. // for this resource.
func ProviderConfigName(t string, pcs map[string]*ProviderConfig) string { func ProviderConfigName(t string, pcs []*ProviderConfig) string {
lk := "" lk := ""
for k, _ := range pcs { for _, v := range pcs {
k := v.Name
if strings.HasPrefix(t, k) && len(k) > len(lk) { if strings.HasPrefix(t, k) && len(k) > len(lk) {
lk = k lk = k
} }
@ -124,6 +127,10 @@ func (c *Config) Validate() error {
} }
vars := c.allVariables() 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 // Check for references to user variables that do not actually
// exist and record those errors. // exist and record those errors.
@ -134,7 +141,7 @@ func (c *Config) Validate() error {
continue continue
} }
if _, ok := c.Variables[uv.Name]; !ok { if _, ok := varMap[uv.Name]; !ok {
errs = append(errs, fmt.Errorf( errs = append(errs, fmt.Errorf(
"%s: unknown variable referenced: %s", "%s: unknown variable referenced: %s",
source, source,
@ -244,6 +251,84 @@ func (c *Config) allVariables() map[string][]InterpolatedVariable {
return result 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. // Required tests whether a variable is required or not.
func (v *Variable) Required() bool { func (v *Variable) Required() bool {
return !v.defaultSet return !v.defaultSet

View File

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

View File

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

View File

@ -2,7 +2,11 @@ package config
import ( import (
"fmt" "fmt"
"io"
"os"
"path/filepath" "path/filepath"
"sort"
"strings"
) )
// Load loads the Terraform configuration from a given file. // 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 // LoadDir loads all the Terraform configuration files in a single
// directory and merges them together. // directory and appends them together.
func LoadDir(path string) (*Config, error) { //
matches, err := filepath.Glob(filepath.Join(path, "*.tf")) // 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 { if err != nil {
return nil, err 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( return nil, fmt.Errorf(
"No Terraform configuration files found in directory: %s", "No Terraform configuration files found in directory: %s",
path) root)
} }
var result *Config 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) c, err := Load(f)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if result != nil { if result != nil {
result, err = Merge(result, c) result, err = Append(result, c)
if err != nil { if err != nil {
return nil, err 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 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,21 +45,26 @@ func (t *libuclConfigurable) Config() (*Config, error) {
// Start building up the actual configuration. We start with // Start building up the actual configuration. We start with
// variables. // variables.
// TODO(mitchellh): Make function like loadVariablesLibucl so that
// duplicates aren't overriden
config := new(Config) config := new(Config)
config.Variables = make(map[string]*Variable) if len(rawConfig.Variable) > 0 {
for k, v := range rawConfig.Variable { config.Variables = make([]*Variable, 0, len(rawConfig.Variable))
defaultSet := false for k, v := range rawConfig.Variable {
for _, f := range v.Fields { defaultSet := false
if f == "Default" { for _, f := range v.Fields {
defaultSet = true if f == "Default" {
break defaultSet = true
break
}
} }
}
config.Variables[k] = &Variable{ config.Variables = append(config.Variables, &Variable{
Default: v.Default, Name: k,
Description: v.Description, Default: v.Default,
defaultSet: defaultSet, 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 // LoadOutputsLibucl recurses into the given libucl object and turns
// it into a mapping of outputs. // 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) objects := make(map[string]*libucl.Object)
// Iterate over all the "output" blocks and get the keys along with // 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() 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. // 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 { for n, o := range objects {
var config map[string]interface{} var config map[string]interface{}
@ -213,10 +223,10 @@ func loadOutputsLibucl(o *libucl.Object) (map[string]*Output, error) {
err) err)
} }
result[n] = &Output{ result = append(result, &Output{
Name: n, Name: n,
RawConfig: rawConfig, RawConfig: rawConfig,
} })
} }
return result, nil 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 // LoadProvidersLibucl recurses into the given libucl object and turns
// it into a mapping of provider configs. // 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) objects := make(map[string]*libucl.Object)
// Iterate over all the "provider" blocks and get the keys along with // 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() iter.Close()
if len(objects) == 0 {
return nil, nil
}
// Go through each object and turn it into an actual result. // 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 { for n, o := range objects {
var config map[string]interface{} var config map[string]interface{}
@ -259,9 +273,10 @@ func loadProvidersLibucl(o *libucl.Object) (map[string]*ProviderConfig, error) {
err) err)
} }
result[n] = &ProviderConfig{ result = append(result, &ProviderConfig{
Name: n,
RawConfig: rawConfig, RawConfig: rawConfig,
} })
} }
return result, nil return result, nil

View File

@ -116,16 +116,6 @@ func TestLoad_variables(t *testing.T) {
if actual != strings.TrimSpace(variablesVariablesStr) { if actual != strings.TrimSpace(variablesVariablesStr) {
t.Fatalf("bad:\n%s", actual) 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) { 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)) ns := make([]string, 0, len(os))
for n, _ := range os { m := make(map[string]*Output)
ns = append(ns, n) for _, o := range os {
ns = append(ns, o.Name)
m[o.Name] = o
} }
sort.Strings(ns) sort.Strings(ns)
result := "" result := ""
for _, n := range ns { for _, n := range ns {
o := os[n] o := m[n]
result += fmt.Sprintf("%s\n", 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 // This helper turns a provider configs field into a deterministic
// string value for comparison in tests. // string value for comparison in tests.
func providerConfigsStr(pcs map[string]*ProviderConfig) string { func providerConfigsStr(pcs []*ProviderConfig) string {
result := "" result := ""
ns := make([]string, 0, len(pcs)) ns := make([]string, 0, len(pcs))
for n, _ := range pcs { m := make(map[string]*ProviderConfig)
ns = append(ns, n) for _, n := range pcs {
ns = append(ns, n.Name)
m[n.Name] = n
} }
sort.Strings(ns) sort.Strings(ns)
for _, n := range ns { for _, n := range ns {
pc := pcs[n] pc := m[n]
result += fmt.Sprintf("%s\n", 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 // This helper turns a variables field into a deterministic
// string value for comparison in tests. // string value for comparison in tests.
func variablesStr(vs map[string]*Variable) string { func variablesStr(vs []*Variable) string {
result := "" result := ""
ks := make([]string, 0, len(vs)) ks := make([]string, 0, len(vs))
for k, _ := range vs { m := make(map[string]*Variable)
ks = append(ks, k) for _, v := range vs {
ks = append(ks, v.Name)
m[v.Name] = v
} }
sort.Strings(ks) sort.Strings(ks)
for _, k := range ks { for _, k := range ks {
v := vs[k] v := m[k]
if v.Default == "" { if v.Default == "" {
v.Default = "<>" v.Default = "<>"
@ -402,9 +444,15 @@ func variablesStr(vs map[string]*Variable) string {
v.Description = "<>" v.Description = "<>"
} }
required := ""
if v.Required() {
required = " (required)"
}
result += fmt.Sprintf( result += fmt.Sprintf(
"%s\n %s\n %s\n", "%s%s\n %s\n %s\n",
k, k,
required,
v.Default, v.Default,
v.Description) v.Description)
} }
@ -486,8 +534,46 @@ foo
bar 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 = ` const importProvidersStr = `
aws aws
bar
foo foo
` `
@ -497,7 +583,7 @@ aws_security_group[web] (x1)
` `
const importVariablesStr = ` const importVariablesStr = `
bar bar (required)
<> <>
<> <>
foo foo
@ -538,7 +624,7 @@ bar
baz baz
foo foo
<> <>
foo foo (required)
<> <>
<> <>
` `

View File

@ -1,9 +1,5 @@
package config package config
import (
"fmt"
)
// Merge merges two configurations into a single configuration. // Merge merges two configurations into a single configuration.
// //
// Merge allows for the two configurations to have duplicate resources, // 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) c.unknownKeys = append(c.unknownKeys, k)
} }
// Merge variables: Variable merging is quite simple. Set fields in // NOTE: Everything below is pretty gross. Due to the lack of generics
// later set variables override those earlier. // in Go, there is some hoop-jumping involved to make this merging a
c.Variables = c1.Variables // little more test-friendly and less repetitive. Ironically, making it
for k, v2 := range c2.Variables { // less repetitive involves being a little repetitive, but I prefer to
v1, ok := c.Variables[k] // be repetitive with things that are less error prone than things that
if ok { // are more error prone (more logic). Type conversions to an interface
if v2.Default == "" { // are pretty low-error.
v2.Default = v1.Default
} var m1, m2, mresult []merger
if v2.Description == "" {
v2.Description = v1.Description // Outputs
} m1 = make([]merger, 0, len(c1.Outputs))
m2 = make([]merger, 0, len(c2.Outputs))
for _, v := range c1.Outputs {
m1 = append(m1, v)
}
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
} }
// Merge outputs: If they collide, just take the latest one for now. In // Provider Configs
// the future, we might provide smarter merge functionality. m1 = make([]merger, 0, len(c1.ProviderConfigs))
c.Outputs = make(map[string]*Output) m2 = make([]merger, 0, len(c2.ProviderConfigs))
for k, v := range c1.Outputs { for _, v := range c1.ProviderConfigs {
c.Outputs[k] = v m1 = append(m1, v)
} }
for k, v := range c2.Outputs { for _, v := range c2.ProviderConfigs {
c.Outputs[k] = v 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 provider configs: If they collide, we just take the latest one // Resources
// for now. In the future, we might provide smarter merge functionality. m1 = make([]merger, 0, len(c1.Resources))
c.ProviderConfigs = make(map[string]*ProviderConfig) m2 = make([]merger, 0, len(c2.Resources))
for k, v := range c1.ProviderConfigs { for _, v := range c1.Resources {
c.ProviderConfigs[k] = v m1 = append(m1, v)
} }
for k, v := range c2.ProviderConfigs { for _, v := range c2.Resources {
c.ProviderConfigs[k] = v 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)
}
} }
// Merge resources: If they collide, we just take the latest one // Variables
// for now. In the future, we might provide smarter merge functionality. m1 = make([]merger, 0, len(c1.Variables))
resources := make(map[string]*Resource) m2 = make([]merger, 0, len(c2.Variables))
for _, r := range c1.Resources { for _, v := range c1.Variables {
id := fmt.Sprintf("%s[%s]", r.Type, r.Name) m1 = append(m1, v)
resources[id] = r
} }
for _, r := range c2.Resources { for _, v := range c2.Variables {
id := fmt.Sprintf("%s[%s]", r.Type, r.Name) m2 = append(m2, v)
resources[id] = r
} }
mresult = mergeSlice(m1, m2)
c.Resources = make([]*Resource, 0, len(resources)) if len(mresult) > 0 {
for _, r := range resources { c.Variables = make([]*Variable, len(mresult))
c.Resources = append(c.Resources, r) for i, v := range mresult {
c.Variables[i] = v.(*Variable)
}
} }
return c, nil 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 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 // UnknownKeys returns the keys of the configuration that are unknown
// because they had interpolated variables that must be computed. // because they had interpolated variables that must be computed.
func (r *RawConfig) UnknownKeys() []string { 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 // Look up the provider config for this resource
pcName := config.ProviderConfigName(resourceNode.Type, c.ProviderConfigs) pcName := config.ProviderConfigName(
resourceNode.Type, c.ProviderConfigs)
if pcName == "" { if pcName == "" {
continue 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 // We have one, so build the noun if it hasn't already been made
pcNoun, ok := pcNouns[pcName] pcNoun, ok := pcNouns[pcName]
if !ok { 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{ pcNoun = &depgraph.Noun{
Name: fmt.Sprintf("provider.%s", pcName), Name: fmt.Sprintf("provider.%s", pcName),
Meta: &GraphNodeResourceProvider{ Meta: &GraphNodeResourceProvider{
ID: pcName, ID: pcName,
Config: c.ProviderConfigs[pcName], Config: pc,
}, },
} }
pcNouns[pcName] = pcNoun pcNouns[pcName] = pcNoun

View File

@ -53,6 +53,8 @@ func TestReadWritePlan(t *testing.T) {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
println(reflect.DeepEqual(actual.Config.Variables, plan.Config.Variables))
if !reflect.DeepEqual(actual, plan) { if !reflect.DeepEqual(actual, plan) {
t.Fatalf("bad: %#v", actual) 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 // Check that all required variables are present
required := make(map[string]struct{}) required := make(map[string]struct{})
for k, v := range c.Variables { for _, v := range c.Variables {
if v.Required() { if v.Required() {
required[k] = struct{}{} required[v.Name] = struct{}{}
} }
} }
for k, _ := range vs { for k, _ := range vs {