config: Merge works properly

This commit is contained in:
Mitchell Hashimoto 2014-07-20 17:17:03 -07:00
parent cf1f3a9e73
commit 9d2e83d56d
5 changed files with 363 additions and 66 deletions

View File

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

@ -505,6 +505,7 @@ foo
const importProvidersStr = ` const importProvidersStr = `
aws aws
bar
foo foo
` `

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,81 +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
if len(c1.Variables) > 0 || len(c2.Variables) > 0 { // little more test-friendly and less repetitive. Ironically, making it
c.Variables = make([]*Variable, 0, len(c1.Variables)+len(c2.Variables)) // less repetitive involves being a little repetitive, but I prefer to
varMap := make(map[string]*Variable) // be repetitive with things that are less error prone than things that
for _, v := range c1.Variables { // are more error prone (more logic). Type conversions to an interface
varMap[v.Name] = v // are pretty low-error.
}
for _, v2 := range c2.Variables {
v1, ok := varMap[v2.Name]
if ok {
if v2.Default == "" {
v2.Default = v1.Default
}
if v2.Description == "" {
v2.Description = v1.Description
}
}
varMap[v2.Name] = v2 var m1, m2, mresult []merger
}
for _, v := range varMap {
c.Variables = append(c.Variables, v)
}
}
// Merge outputs: If they collide, just take the latest one for now. In // Outputs
// the future, we might provide smarter merge functionality. m1 = make([]merger, 0, len(c1.Outputs))
if len(c1.Outputs) > 0 || len(c2.Outputs) > 0 { m2 = make([]merger, 0, len(c2.Outputs))
c.Outputs = make([]*Output, 0, len(c1.Outputs)+len(c2.Outputs))
m := make(map[string]*Output)
for _, v := range c1.Outputs { for _, v := range c1.Outputs {
m[v.Name] = v m1 = append(m1, v)
} }
for _, v := range c2.Outputs { for _, v := range c2.Outputs {
m[v.Name] = v m2 = append(m2, v)
} }
for _, v := range m { mresult = mergeSlice(m1, m2)
c.Outputs = append(c.Outputs, v) if len(mresult) > 0 {
c.Outputs = make([]*Output, len(mresult))
for i, v := range mresult {
c.Outputs[i] = v.(*Output)
} }
} }
// Merge provider configs: If they collide, we just take the latest one // Provider Configs
// for now. In the future, we might provide smarter merge functionality. m1 = make([]merger, 0, len(c1.ProviderConfigs))
if len(c1.ProviderConfigs) > 0 || len(c2.ProviderConfigs) > 0 { m2 = make([]merger, 0, len(c2.ProviderConfigs))
m := make(map[string]*ProviderConfig)
for _, v := range c1.ProviderConfigs { for _, v := range c1.ProviderConfigs {
m[v.Name] = v m1 = append(m1, v)
} }
for _, v := range c2.ProviderConfigs { for _, v := range c2.ProviderConfigs {
m[v.Name] = v m2 = append(m2, v)
} }
mresult = mergeSlice(m1, m2)
c.ProviderConfigs = make([]*ProviderConfig, 0, len(m)) if len(mresult) > 0 {
for _, v := range m { c.ProviderConfigs = make([]*ProviderConfig, len(mresult))
c.ProviderConfigs = append(c.ProviderConfigs, v) for i, v := range mresult {
c.ProviderConfigs[i] = v.(*ProviderConfig)
} }
} }
// Merge resources: 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))
resources := make(map[string]*Resource) m2 = make([]merger, 0, len(c2.Resources))
for _, r := range c1.Resources { for _, v := range c1.Resources {
id := fmt.Sprintf("%s[%s]", r.Type, r.Name) m1 = append(m1, v)
resources[id] = r }
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 _, r := range c2.Resources {
id := fmt.Sprintf("%s[%s]", r.Type, r.Name)
resources[id] = r
} }
c.Resources = make([]*Resource, 0, len(resources)) // Variables
for _, r := range resources { m1 = make([]merger, 0, len(c1.Variables))
c.Resources = append(c.Resources, r) m2 = make([]merger, 0, len(c2.Variables))
for _, v := range c1.Variables {
m1 = append(m1, v)
}
for _, v := range c2.Variables {
m2 = append(m2, v)
}
mresult = mergeSlice(m1, m2)
if len(mresult) > 0 {
c.Variables = make([]*Variable, len(mresult))
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 {