Merge pull request #26 from hashicorp/f-override
Configuration Overrides
This commit is contained in:
commit
3b6ef5d3ac
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
<>
|
||||
<>
|
||||
`
|
||||
|
|
160
config/merge.go
160
config/merge.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
variable "foo" {
|
||||
default = "bar";
|
||||
description = "bar";
|
||||
}
|
||||
|
||||
resource "aws_instance" "db" {
|
||||
security_groups = "${aws_security_group.firewall.*.id}"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
resource "aws_instance" "db" {
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"resource": {
|
||||
"aws_instance": {
|
||||
"web": {
|
||||
"foo": "bar",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"resource": {
|
||||
"aws_instance": {
|
||||
"db": {
|
||||
"ami": "foo",
|
||||
"security_groups": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue