diff --git a/Makefile b/Makefile index ec8a825000..7520a245a7 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ dev: config/y.go @TF_DEV=1 sh -c "$(CURDIR)/scripts/build.sh" test: config/y.go - TF_ACC= go test $(TEST) $(TESTARGS) -timeout=10s + TF_ACC= go test $(TEST) $(TESTARGS) -timeout=10s -parallel=4 testacc: config/y.go @if [ "$(TEST)" = "./..." ]; then \ diff --git a/config/append.go b/config/append.go index de7b2c8247..f87e677480 100644 --- a/config/append.go +++ b/config/append.go @@ -29,6 +29,13 @@ func Append(c1, c2 *Config) (*Config, error) { } } + if len(c1.Modules) > 0 || len(c2.Modules) > 0 { + c.Modules = make( + []*Module, 0, len(c1.Modules)+len(c2.Modules)) + c.Modules = append(c.Modules, c1.Modules...) + c.Modules = append(c.Modules, c2.Modules...) + } + if len(c1.Outputs) > 0 || len(c2.Outputs) > 0 { c.Outputs = make( []*Output, 0, len(c1.Outputs)+len(c2.Outputs)) diff --git a/config/append_test.go b/config/append_test.go index 4a7fb9d1e7..e7aea9d214 100644 --- a/config/append_test.go +++ b/config/append_test.go @@ -12,6 +12,9 @@ func TestAppend(t *testing.T) { }{ { &Config{ + Modules: []*Module{ + &Module{Name: "foo"}, + }, Outputs: []*Output{ &Output{Name: "foo"}, }, @@ -29,6 +32,9 @@ func TestAppend(t *testing.T) { }, &Config{ + Modules: []*Module{ + &Module{Name: "bar"}, + }, Outputs: []*Output{ &Output{Name: "bar"}, }, @@ -46,6 +52,10 @@ func TestAppend(t *testing.T) { }, &Config{ + Modules: []*Module{ + &Module{Name: "foo"}, + &Module{Name: "bar"}, + }, Outputs: []*Output{ &Output{Name: "foo"}, &Output{Name: "bar"}, diff --git a/config/config.go b/config/config.go index 102e181631..b4b405107e 100644 --- a/config/config.go +++ b/config/config.go @@ -15,6 +15,12 @@ import ( // Config is the configuration that comes from loading a collection // of Terraform templates. type Config struct { + // Dir is the path to the directory where this configuration was + // loaded from. If it is blank, this configuration wasn't loaded from + // any meaningful directory. + Dir string + + Modules []*Module ProviderConfigs []*ProviderConfig Resources []*Resource Variables []*Variable @@ -25,6 +31,16 @@ type Config struct { unknownKeys []string } +// Module is a module used within a configuration. +// +// This does not represent a module itself, this represents a module +// call-site within an existing configuration. +type Module struct { + Name string + Source string + RawConfig *RawConfig +} + // ProviderConfig is the configuration for a resource provider. // // For example, Terraform needs to set the AWS access keys for the AWS @@ -92,6 +108,11 @@ func ProviderConfigName(t string, pcs []*ProviderConfig) string { return lk } +// A unique identifier for this module. +func (r *Module) Id() string { + return fmt.Sprintf("%s", r.Name) +} + // A unique identifier for this resource. func (r *Resource) Id() string { return fmt.Sprintf("%s.%s", r.Type, r.Name) @@ -106,7 +127,7 @@ func (c *Config) Validate() error { "Unknown root level key: %s", k)) } - vars := c.allVariables() + vars := c.InterpolatedVariables() varMap := make(map[string]*Variable) for _, v := range c.Variables { varMap[v.Name] = v @@ -156,9 +177,45 @@ func (c *Config) Validate() error { } } + // Check that all references to modules are valid + modules := make(map[string]*Module) + dupped := make(map[string]struct{}) + for _, m := range c.Modules { + if _, ok := modules[m.Id()]; ok { + if _, ok := dupped[m.Id()]; !ok { + dupped[m.Id()] = struct{}{} + + errs = append(errs, fmt.Errorf( + "%s: module repeated multiple times", + m.Id())) + } + } + + modules[m.Id()] = m + } + dupped = nil + + // Check that all variables for modules reference modules that + // exist. + for source, vs := range vars { + for _, v := range vs { + mv, ok := v.(*ModuleVariable) + if !ok { + continue + } + + if _, ok := modules[mv.Name]; !ok { + errs = append(errs, fmt.Errorf( + "%s: unknown module referenced: %s", + source, + mv.Name)) + } + } + } + // Check that all references to resources are valid resources := make(map[string]*Resource) - dupped := make(map[string]struct{}) + dupped = make(map[string]struct{}) for _, r := range c.Resources { if _, ok := resources[r.Id()]; ok { if _, ok := dupped[r.Id()]; !ok { @@ -245,10 +302,10 @@ func (c *Config) Validate() error { return nil } -// allVariables is a helper that returns a mapping of all the interpolated +// InterpolatedVariables is a helper that returns a mapping of all the interpolated // variables within the configuration. This is used to verify references // are valid in the Validate step. -func (c *Config) allVariables() map[string][]InterpolatedVariable { +func (c *Config) InterpolatedVariables() map[string][]InterpolatedVariable { result := make(map[string][]InterpolatedVariable) for _, pc := range c.ProviderConfigs { source := fmt.Sprintf("provider config '%s'", pc.Name) @@ -274,6 +331,24 @@ func (c *Config) allVariables() map[string][]InterpolatedVariable { return result } +func (m *Module) mergerName() string { + return m.Id() +} + +func (m *Module) mergerMerge(other merger) merger { + m2 := other.(*Module) + + result := *m + result.Name = m2.Name + result.RawConfig = result.RawConfig.merge(m2.RawConfig) + + if m2.Source != "" { + result.Source = m2.Source + } + + return &result +} + func (o *Output) mergerName() string { return o.Name } diff --git a/config/config_string.go b/config/config_string.go new file mode 100644 index 0000000000..89a990ae82 --- /dev/null +++ b/config/config_string.go @@ -0,0 +1,297 @@ +package config + +import ( + "bytes" + "fmt" + "sort" + "strings" +) + +// TestString is a Stringer-like function that outputs a string that can +// be used to easily compare multiple Config structures in unit tests. +// +// This function has no practical use outside of unit tests and debugging. +func (c *Config) TestString() string { + if c == nil { + return "" + } + + var buf bytes.Buffer + if len(c.Modules) > 0 { + buf.WriteString("Modules:\n\n") + buf.WriteString(modulesStr(c.Modules)) + buf.WriteString("\n\n") + } + + if len(c.Variables) > 0 { + buf.WriteString("Variables:\n\n") + buf.WriteString(variablesStr(c.Variables)) + buf.WriteString("\n\n") + } + + if len(c.ProviderConfigs) > 0 { + buf.WriteString("Provider Configs:\n\n") + buf.WriteString(providerConfigsStr(c.ProviderConfigs)) + buf.WriteString("\n\n") + } + + if len(c.Resources) > 0 { + buf.WriteString("Resources:\n\n") + buf.WriteString(resourcesStr(c.Resources)) + buf.WriteString("\n\n") + } + + if len(c.Outputs) > 0 { + buf.WriteString("Outputs:\n\n") + buf.WriteString(outputsStr(c.Outputs)) + buf.WriteString("\n") + } + + return strings.TrimSpace(buf.String()) +} + +func modulesStr(ms []*Module) string { + result := "" + order := make([]int, 0, len(ms)) + ks := make([]string, 0, len(ms)) + mapping := make(map[string]int) + for i, m := range ms { + k := m.Id() + ks = append(ks, k) + mapping[k] = i + } + sort.Strings(ks) + for _, k := range ks { + order = append(order, mapping[k]) + } + + for _, i := range order { + m := ms[i] + result += fmt.Sprintf("%s\n", m.Id()) + + ks := make([]string, 0, len(m.RawConfig.Raw)) + for k, _ := range m.RawConfig.Raw { + ks = append(ks, k) + } + sort.Strings(ks) + + result += fmt.Sprintf(" source = %s\n", m.Source) + + for _, k := range ks { + result += fmt.Sprintf(" %s\n", k) + } + } + + return strings.TrimSpace(result) +} + +func outputsStr(os []*Output) string { + ns := make([]string, 0, len(os)) + 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 := m[n] + + result += fmt.Sprintf("%s\n", n) + + if len(o.RawConfig.Variables) > 0 { + result += fmt.Sprintf(" vars\n") + for _, rawV := range o.RawConfig.Variables { + kind := "unknown" + str := rawV.FullKey() + + switch rawV.(type) { + case *ResourceVariable: + kind = "resource" + case *UserVariable: + kind = "user" + } + + result += fmt.Sprintf(" %s: %s\n", kind, str) + } + } + } + + return strings.TrimSpace(result) +} + +// This helper turns a provider configs field into a deterministic +// string value for comparison in tests. +func providerConfigsStr(pcs []*ProviderConfig) string { + result := "" + + ns := make([]string, 0, len(pcs)) + 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 := m[n] + + result += fmt.Sprintf("%s\n", n) + + keys := make([]string, 0, len(pc.RawConfig.Raw)) + for k, _ := range pc.RawConfig.Raw { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + result += fmt.Sprintf(" %s\n", k) + } + + if len(pc.RawConfig.Variables) > 0 { + result += fmt.Sprintf(" vars\n") + for _, rawV := range pc.RawConfig.Variables { + kind := "unknown" + str := rawV.FullKey() + + switch rawV.(type) { + case *ResourceVariable: + kind = "resource" + case *UserVariable: + kind = "user" + } + + result += fmt.Sprintf(" %s: %s\n", kind, str) + } + } + } + + return strings.TrimSpace(result) +} + +// This helper turns a resources field into a deterministic +// string value for comparison in tests. +func resourcesStr(rs []*Resource) string { + result := "" + order := make([]int, 0, len(rs)) + ks := make([]string, 0, len(rs)) + mapping := make(map[string]int) + for i, r := range rs { + k := fmt.Sprintf("%s[%s]", r.Type, r.Name) + ks = append(ks, k) + mapping[k] = i + } + sort.Strings(ks) + for _, k := range ks { + order = append(order, mapping[k]) + } + + for _, i := range order { + r := rs[i] + result += fmt.Sprintf( + "%s[%s] (x%d)\n", + r.Type, + r.Name, + r.Count) + + ks := make([]string, 0, len(r.RawConfig.Raw)) + for k, _ := range r.RawConfig.Raw { + ks = append(ks, k) + } + sort.Strings(ks) + + for _, k := range ks { + result += fmt.Sprintf(" %s\n", k) + } + + if len(r.Provisioners) > 0 { + result += fmt.Sprintf(" provisioners\n") + for _, p := range r.Provisioners { + result += fmt.Sprintf(" %s\n", p.Type) + + ks := make([]string, 0, len(p.RawConfig.Raw)) + for k, _ := range p.RawConfig.Raw { + ks = append(ks, k) + } + sort.Strings(ks) + + for _, k := range ks { + result += fmt.Sprintf(" %s\n", k) + } + } + } + + if len(r.DependsOn) > 0 { + result += fmt.Sprintf(" dependsOn\n") + for _, d := range r.DependsOn { + result += fmt.Sprintf(" %s\n", d) + } + } + + if len(r.RawConfig.Variables) > 0 { + result += fmt.Sprintf(" vars\n") + + ks := make([]string, 0, len(r.RawConfig.Variables)) + for k, _ := range r.RawConfig.Variables { + ks = append(ks, k) + } + sort.Strings(ks) + + for _, k := range ks { + rawV := r.RawConfig.Variables[k] + kind := "unknown" + str := rawV.FullKey() + + switch rawV.(type) { + case *ResourceVariable: + kind = "resource" + case *UserVariable: + kind = "user" + } + + result += fmt.Sprintf(" %s: %s\n", kind, str) + } + } + } + + return strings.TrimSpace(result) +} + +// This helper turns a variables field into a deterministic +// string value for comparison in tests. +func variablesStr(vs []*Variable) string { + result := "" + ks := make([]string, 0, len(vs)) + 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 := m[k] + + required := "" + if v.Required() { + required = " (required)" + } + + if v.Default == nil || v.Default == "" { + v.Default = "<>" + } + if v.Description == "" { + v.Description = "<>" + } + + result += fmt.Sprintf( + "%s%s\n %v\n %s\n", + k, + required, + v.Default, + v.Description) + } + + return strings.TrimSpace(result) +} diff --git a/config/config_test.go b/config/config_test.go index 59242277b2..26becdd30b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -44,6 +44,13 @@ func TestConfigValidate_countZero(t *testing.T) { } } +func TestConfigValidate_dupModule(t *testing.T) { + c := testConfig(t, "validate-dup-module") + if err := c.Validate(); err == nil { + t.Fatal("should not be valid") + } +} + func TestConfigValidate_dupResource(t *testing.T) { c := testConfig(t, "validate-dup-resource") if err := c.Validate(); err == nil { @@ -107,6 +114,20 @@ func TestConfigValidate_varDefaultInterpolate(t *testing.T) { } } +func TestConfigValidate_varModule(t *testing.T) { + c := testConfig(t, "validate-var-module") + if err := c.Validate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestConfigValidate_varModuleInvalid(t *testing.T) { + c := testConfig(t, "validate-var-module-invalid") + if err := c.Validate(); err == nil { + t.Fatal("should not be valid") + } +} + func TestProviderConfigName(t *testing.T) { pcs := []*ProviderConfig{ &ProviderConfig{Name: "aw"}, diff --git a/config/expr_parse_test.go b/config/expr_parse_test.go index a8d34d5534..da77c7bdae 100644 --- a/config/expr_parse_test.go +++ b/config/expr_parse_test.go @@ -34,6 +34,18 @@ func TestExprParse(t *testing.T) { false, }, + { + "module.foo.bar", + &VariableInterpolation{ + Variable: &ModuleVariable{ + Name: "foo", + Field: "bar", + key: "module.foo.bar", + }, + }, + false, + }, + { "lookup(var.foo, var.bar)", &FunctionInterpolation{ diff --git a/config/interpolate.go b/config/interpolate.go index cffd58b179..b6d84d4256 100644 --- a/config/interpolate.go +++ b/config/interpolate.go @@ -52,6 +52,14 @@ type VariableInterpolation struct { Variable InterpolatedVariable } +// A ModuleVariable is a variable that is referencing the output +// of a module, such as "${module.foo.bar}" +type ModuleVariable struct { + Name string + Field string + key string +} + // A ResourceVariable is a variable that is referencing the field // of a resource, such as "${aws_instance.foo.ami}" type ResourceVariable struct { @@ -76,11 +84,13 @@ type UserVariable struct { } func NewInterpolatedVariable(v string) (InterpolatedVariable, error) { - if !strings.HasPrefix(v, "var.") { + if strings.HasPrefix(v, "var.") { + return NewUserVariable(v) + } else if strings.HasPrefix(v, "module.") { + return NewModuleVariable(v) + } else { return NewResourceVariable(v) } - - return NewUserVariable(v) } func (i *FunctionInterpolation) Interpolate( @@ -142,6 +152,25 @@ func (i *VariableInterpolation) Variables() map[string]InterpolatedVariable { return map[string]InterpolatedVariable{i.Variable.FullKey(): i.Variable} } +func NewModuleVariable(key string) (*ModuleVariable, error) { + parts := strings.SplitN(key, ".", 3) + if len(parts) < 3 { + return nil, fmt.Errorf( + "%s: module variables must be three parts: module.name.attr", + key) + } + + return &ModuleVariable{ + Name: parts[1], + Field: parts[2], + key: key, + }, nil +} + +func (v *ModuleVariable) FullKey() string { + return v.key +} + func NewResourceVariable(key string) (*ResourceVariable, error) { parts := strings.SplitN(key, ".", 3) if len(parts) < 3 { diff --git a/config/interpolate_test.go b/config/interpolate_test.go index 92bb0f15cb..61d188d22e 100644 --- a/config/interpolate_test.go +++ b/config/interpolate_test.go @@ -20,6 +20,15 @@ func TestNewInterpolatedVariable(t *testing.T) { }, false, }, + { + "module.foo.bar", + &ModuleVariable{ + Name: "foo", + Field: "bar", + key: "module.foo.bar", + }, + false, + }, } for i, tc := range cases { diff --git a/config/loader.go b/config/loader.go index 7b3112ad6d..5cc27e998c 100644 --- a/config/loader.go +++ b/config/loader.go @@ -104,6 +104,12 @@ func LoadDir(root string) (*Config, error) { root) } + // Determine the absolute path to the directory. + rootAbs, err := filepath.Abs(root) + if err != nil { + return nil, err + } + var result *Config // Sort the files and overrides so we have a deterministic order @@ -140,6 +146,9 @@ func LoadDir(root string) (*Config, error) { } } + // Mark the directory + result.Dir = rootAbs + return result, nil } diff --git a/config/loader_hcl.go b/config/loader_hcl.go index 380d50294b..babc34ca62 100644 --- a/config/loader_hcl.go +++ b/config/loader_hcl.go @@ -17,6 +17,7 @@ type hclConfigurable struct { func (t *hclConfigurable) Config() (*Config, error) { validKeys := map[string]struct{}{ + "module": struct{}{}, "output": struct{}{}, "provider": struct{}{}, "resource": struct{}{}, @@ -69,6 +70,15 @@ func (t *hclConfigurable) Config() (*Config, error) { } } + // Build the modules + if modules := t.Object.Get("module", false); modules != nil { + var err error + config.Modules, err = loadModulesHcl(modules) + if err != nil { + return nil, err + } + } + // Build the provider configs if providers := t.Object.Get("provider", false); providers != nil { var err error @@ -177,6 +187,75 @@ func loadFileHcl(root string) (configurable, []string, error) { return result, nil, nil } +// Given a handle to a HCL object, this recurses into the structure +// and pulls out a list of modules. +// +// The resulting modules may not be unique, but each module +// represents exactly one module definition in the HCL configuration. +// We leave it up to another pass to merge them together. +func loadModulesHcl(os *hclobj.Object) ([]*Module, error) { + var allNames []*hclobj.Object + + // See loadResourcesHcl for why this exists. Don't touch this. + for _, o1 := range os.Elem(false) { + // Iterate the inner to get the list of types + for _, o2 := range o1.Elem(true) { + // Iterate all of this type to get _all_ the types + for _, o3 := range o2.Elem(false) { + allNames = append(allNames, o3) + } + } + } + + // Where all the results will go + var result []*Module + + // Now go over all the types and their children in order to get + // all of the actual resources. + for _, obj := range allNames { + k := obj.Key + + var config map[string]interface{} + if err := hcl.DecodeObject(&config, obj); err != nil { + return nil, fmt.Errorf( + "Error reading config for %s: %s", + k, + err) + } + + // Remove the fields we handle specially + delete(config, "source") + + rawConfig, err := NewRawConfig(config) + if err != nil { + return nil, fmt.Errorf( + "Error reading config for %s: %s", + k, + err) + } + + // If we have a count, then figure it out + var source string + if o := obj.Get("source", false); o != nil { + err = hcl.DecodeObject(&source, o) + if err != nil { + return nil, fmt.Errorf( + "Error parsing source for %s: %s", + k, + err) + } + } + + result = append(result, &Module{ + Name: k, + Source: source, + RawConfig: rawConfig, + }) + } + + return result, nil +} + // LoadOutputsHcl recurses into the given HCL object and turns // it into a mapping of outputs. func loadOutputsHcl(os *hclobj.Object) ([]*Output, error) { diff --git a/config/loader_test.go b/config/loader_test.go index b20096d6fd..f95235d66e 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -1,9 +1,7 @@ package config import ( - "fmt" "path/filepath" - "sort" "strings" "testing" ) @@ -25,6 +23,10 @@ func TestLoadBasic(t *testing.T) { t.Fatal("config should not be nil") } + if c.Dir != "" { + t.Fatalf("bad: %#v", c.Dir) + } + actual := variablesStr(c.Variables) if actual != strings.TrimSpace(basicVariablesStr) { t.Fatalf("bad:\n%s", actual) @@ -46,6 +48,17 @@ func TestLoadBasic(t *testing.T) { } } +func TestLoadBasic_empty(t *testing.T) { + c, err := Load(filepath.Join(fixtureDir, "empty.tf")) + if err != nil { + t.Fatalf("err: %s", err) + } + + if c == nil { + t.Fatal("config should not be nil") + } +} + func TestLoadBasic_import(t *testing.T) { // Skip because we disabled importing t.Skip() @@ -85,6 +98,10 @@ func TestLoadBasic_json(t *testing.T) { t.Fatal("config should not be nil") } + if c.Dir != "" { + t.Fatalf("bad: %#v", c.Dir) + } + actual := variablesStr(c.Variables) if actual != strings.TrimSpace(basicVariablesStr) { t.Fatalf("bad:\n%s", actual) @@ -106,6 +123,26 @@ func TestLoadBasic_json(t *testing.T) { } } +func TestLoadBasic_modules(t *testing.T) { + c, err := Load(filepath.Join(fixtureDir, "modules.tf")) + if err != nil { + t.Fatalf("err: %s", err) + } + + if c == nil { + t.Fatal("config should not be nil") + } + + if c.Dir != "" { + t.Fatalf("bad: %#v", c.Dir) + } + + actual := modulesStr(c.Modules) + if actual != strings.TrimSpace(modulesModulesStr) { + t.Fatalf("bad:\n%s", actual) + } +} + func TestLoad_variables(t *testing.T) { c, err := Load(filepath.Join(fixtureDir, "variables.tf")) if err != nil { @@ -115,6 +152,10 @@ func TestLoad_variables(t *testing.T) { t.Fatal("config should not be nil") } + if c.Dir != "" { + t.Fatalf("bad: %#v", c.Dir) + } + actual := variablesStr(c.Variables) if actual != strings.TrimSpace(variablesVariablesStr) { t.Fatalf("bad:\n%s", actual) @@ -122,7 +163,8 @@ func TestLoad_variables(t *testing.T) { } func TestLoadDir_basic(t *testing.T) { - c, err := LoadDir(filepath.Join(fixtureDir, "dir-basic")) + dir := filepath.Join(fixtureDir, "dir-basic") + c, err := LoadDir(dir) if err != nil { t.Fatalf("err: %s", err) } @@ -131,6 +173,14 @@ func TestLoadDir_basic(t *testing.T) { t.Fatal("config should not be nil") } + dirAbs, err := filepath.Abs(dir) + if err != nil { + t.Fatalf("err: %s", err) + } + if c.Dir != dirAbs { + t.Fatalf("bad: %#v", c.Dir) + } + actual := variablesStr(c.Variables) if actual != strings.TrimSpace(dirBasicVariablesStr) { t.Fatalf("bad:\n%s", actual) @@ -212,42 +262,6 @@ func TestLoadDir_override(t *testing.T) { } } -func outputsStr(os []*Output) string { - ns := make([]string, 0, len(os)) - 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 := m[n] - - result += fmt.Sprintf("%s\n", n) - - if len(o.RawConfig.Variables) > 0 { - result += fmt.Sprintf(" vars\n") - for _, rawV := range o.RawConfig.Variables { - kind := "unknown" - str := rawV.FullKey() - - switch rawV.(type) { - case *ResourceVariable: - kind = "resource" - case *UserVariable: - kind = "user" - } - - result += fmt.Sprintf(" %s: %s\n", kind, str) - } - } - } - - return strings.TrimSpace(result) -} - func TestLoad_provisioners(t *testing.T) { c, err := Load(filepath.Join(fixtureDir, "provisioners.tf")) if err != nil { @@ -302,181 +316,6 @@ 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 []*ProviderConfig) string { - result := "" - - ns := make([]string, 0, len(pcs)) - 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 := m[n] - - result += fmt.Sprintf("%s\n", n) - - keys := make([]string, 0, len(pc.RawConfig.Raw)) - for k, _ := range pc.RawConfig.Raw { - keys = append(keys, k) - } - sort.Strings(keys) - - for _, k := range keys { - result += fmt.Sprintf(" %s\n", k) - } - - if len(pc.RawConfig.Variables) > 0 { - result += fmt.Sprintf(" vars\n") - for _, rawV := range pc.RawConfig.Variables { - kind := "unknown" - str := rawV.FullKey() - - switch rawV.(type) { - case *ResourceVariable: - kind = "resource" - case *UserVariable: - kind = "user" - } - - result += fmt.Sprintf(" %s: %s\n", kind, str) - } - } - } - - return strings.TrimSpace(result) -} - -// This helper turns a resources field into a deterministic -// string value for comparison in tests. -func resourcesStr(rs []*Resource) string { - result := "" - order := make([]int, 0, len(rs)) - ks := make([]string, 0, len(rs)) - mapping := make(map[string]int) - for i, r := range rs { - k := fmt.Sprintf("%s[%s]", r.Type, r.Name) - ks = append(ks, k) - mapping[k] = i - } - sort.Strings(ks) - for _, k := range ks { - order = append(order, mapping[k]) - } - - for _, i := range order { - r := rs[i] - result += fmt.Sprintf( - "%s[%s] (x%d)\n", - r.Type, - r.Name, - r.Count) - - ks := make([]string, 0, len(r.RawConfig.Raw)) - for k, _ := range r.RawConfig.Raw { - ks = append(ks, k) - } - sort.Strings(ks) - - for _, k := range ks { - result += fmt.Sprintf(" %s\n", k) - } - - if len(r.Provisioners) > 0 { - result += fmt.Sprintf(" provisioners\n") - for _, p := range r.Provisioners { - result += fmt.Sprintf(" %s\n", p.Type) - - ks := make([]string, 0, len(p.RawConfig.Raw)) - for k, _ := range p.RawConfig.Raw { - ks = append(ks, k) - } - sort.Strings(ks) - - for _, k := range ks { - result += fmt.Sprintf(" %s\n", k) - } - } - } - - if len(r.DependsOn) > 0 { - result += fmt.Sprintf(" dependsOn\n") - for _, d := range r.DependsOn { - result += fmt.Sprintf(" %s\n", d) - } - } - - if len(r.RawConfig.Variables) > 0 { - result += fmt.Sprintf(" vars\n") - - ks := make([]string, 0, len(r.RawConfig.Variables)) - for k, _ := range r.RawConfig.Variables { - ks = append(ks, k) - } - sort.Strings(ks) - - for _, k := range ks { - rawV := r.RawConfig.Variables[k] - kind := "unknown" - str := rawV.FullKey() - - switch rawV.(type) { - case *ResourceVariable: - kind = "resource" - case *UserVariable: - kind = "user" - } - - result += fmt.Sprintf(" %s: %s\n", kind, str) - } - } - } - - return strings.TrimSpace(result) -} - -// This helper turns a variables field into a deterministic -// string value for comparison in tests. -func variablesStr(vs []*Variable) string { - result := "" - ks := make([]string, 0, len(vs)) - 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 := m[k] - - required := "" - if v.Required() { - required = " (required)" - } - - if v.Default == nil || v.Default == "" { - v.Default = "<>" - } - if v.Description == "" { - v.Description = "<>" - } - - result += fmt.Sprintf( - "%s%s\n %v\n %s\n", - k, - required, - v.Default, - v.Description) - } - - return strings.TrimSpace(result) -} - const basicOutputsStr = ` web_ip vars @@ -611,6 +450,12 @@ foo bar ` +const modulesModulesStr = ` +bar + source = baz + memory +` + const provisionerResourcesStr = ` aws_instance[web] (x1) ami diff --git a/config/merge.go b/config/merge.go index 835d652a9c..c43f13c045 100644 --- a/config/merge.go +++ b/config/merge.go @@ -35,6 +35,23 @@ func Merge(c1, c2 *Config) (*Config, error) { var m1, m2, mresult []merger + // Modules + m1 = make([]merger, 0, len(c1.Modules)) + m2 = make([]merger, 0, len(c2.Modules)) + for _, v := range c1.Modules { + m1 = append(m1, v) + } + for _, v := range c2.Modules { + m2 = append(m2, v) + } + mresult = mergeSlice(m1, m2) + if len(mresult) > 0 { + c.Modules = make([]*Module, len(mresult)) + for i, v := range mresult { + c.Modules[i] = v.(*Module) + } + } + // Outputs m1 = make([]merger, 0, len(c1.Outputs)) m2 = make([]merger, 0, len(c2.Outputs)) diff --git a/config/merge_test.go b/config/merge_test.go index 10ef5f37a7..2dbe5aee98 100644 --- a/config/merge_test.go +++ b/config/merge_test.go @@ -13,6 +13,9 @@ func TestMerge(t *testing.T) { // Normal good case. { &Config{ + Modules: []*Module{ + &Module{Name: "foo"}, + }, Outputs: []*Output{ &Output{Name: "foo"}, }, @@ -30,6 +33,9 @@ func TestMerge(t *testing.T) { }, &Config{ + Modules: []*Module{ + &Module{Name: "bar"}, + }, Outputs: []*Output{ &Output{Name: "bar"}, }, @@ -47,6 +53,10 @@ func TestMerge(t *testing.T) { }, &Config{ + Modules: []*Module{ + &Module{Name: "foo"}, + &Module{Name: "bar"}, + }, Outputs: []*Output{ &Output{Name: "foo"}, &Output{Name: "bar"}, diff --git a/config/module/detect.go b/config/module/detect.go new file mode 100644 index 0000000000..99b2af0a73 --- /dev/null +++ b/config/module/detect.go @@ -0,0 +1,68 @@ +package module + +import ( + "fmt" + "net/url" +) + +// Detector defines the interface that an invalid URL or a URL with a blank +// scheme is passed through in order to determine if its shorthand for +// something else well-known. +type Detector interface { + // Detect will detect whether the string matches a known pattern to + // turn it into a proper URL. + Detect(string, string) (string, bool, error) +} + +// Detectors is the list of detectors that are tried on an invalid URL. +// This is also the order they're tried (index 0 is first). +var Detectors []Detector + +func init() { + Detectors = []Detector{ + new(GitHubDetector), + new(BitBucketDetector), + new(FileDetector), + } +} + +// Detect turns a source string into another source string if it is +// detected to be of a known pattern. +// +// This is safe to be called with an already valid source string: Detect +// will just return it. +func Detect(src string, pwd string) (string, error) { + getForce, getSrc := getForcedGetter(src) + + u, err := url.Parse(getSrc) + if err == nil && u.Scheme != "" { + // Valid URL + return src, nil + } + + for _, d := range Detectors { + result, ok, err := d.Detect(getSrc, pwd) + if err != nil { + return "", err + } + if !ok { + continue + } + + var detectForce string + detectForce, result = getForcedGetter(result) + + // Preserve the forced getter if it exists. We try to use the + // original set force first, followed by any force set by the + // detector. + if getForce != "" { + result = fmt.Sprintf("%s::%s", getForce, result) + } else if detectForce != "" { + result = fmt.Sprintf("%s::%s", detectForce, result) + } + + return result, nil + } + + return "", fmt.Errorf("invalid source string: %s", src) +} diff --git a/config/module/detect_bitbucket.go b/config/module/detect_bitbucket.go new file mode 100644 index 0000000000..657637c099 --- /dev/null +++ b/config/module/detect_bitbucket.go @@ -0,0 +1,66 @@ +package module + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" +) + +// BitBucketDetector implements Detector to detect BitBucket URLs and turn +// them into URLs that the Git or Hg Getter can understand. +type BitBucketDetector struct{} + +func (d *BitBucketDetector) Detect(src, _ string) (string, bool, error) { + if len(src) == 0 { + return "", false, nil + } + + if strings.HasPrefix(src, "bitbucket.org/") { + return d.detectHTTP(src) + } + + return "", false, nil +} + +func (d *BitBucketDetector) detectHTTP(src string) (string, bool, error) { + u, err := url.Parse("https://" + src) + if err != nil { + return "", true, fmt.Errorf("error parsing BitBucket URL: %s", err) + } + + // We need to get info on this BitBucket repository to determine whether + // it is Git or Hg. + var info struct { + SCM string `json:"scm"` + } + infoUrl := "https://api.bitbucket.org/1.0/repositories" + u.Path + resp, err := http.Get(infoUrl) + if err != nil { + return "", true, fmt.Errorf("error looking up BitBucket URL: %s", err) + } + if resp.StatusCode == 403 { + // A private repo + return "", true, fmt.Errorf( + "shorthand BitBucket URL can't be used for private repos, " + + "please use a full URL") + } + dec := json.NewDecoder(resp.Body) + if err := dec.Decode(&info); err != nil { + return "", true, fmt.Errorf("error looking up BitBucket URL: %s", err) + } + + switch info.SCM { + case "git": + if !strings.HasSuffix(u.Path, ".git") { + u.Path += ".git" + } + + return "git::" + u.String(), true, nil + case "hg": + return "hg::" + u.String(), true, nil + default: + return "", true, fmt.Errorf("unknown BitBucket SCM type: %s", info.SCM) + } +} diff --git a/config/module/detect_bitbucket_test.go b/config/module/detect_bitbucket_test.go new file mode 100644 index 0000000000..92ce633b14 --- /dev/null +++ b/config/module/detect_bitbucket_test.go @@ -0,0 +1,52 @@ +package module + +import ( + "net/http" + "testing" +) + +const testBBUrl = "https://bitbucket.org/hashicorp/tf-test-git" + +func TestBitBucketDetector(t *testing.T) { + t.Parallel() + + if _, err := http.Get(testBBUrl); err != nil { + t.Log("internet may not be working, skipping BB tests") + t.Skip() + } + + cases := []struct { + Input string + Output string + }{ + // HTTP + { + "bitbucket.org/hashicorp/tf-test-git", + "git::https://bitbucket.org/hashicorp/tf-test-git.git", + }, + { + "bitbucket.org/hashicorp/tf-test-git.git", + "git::https://bitbucket.org/hashicorp/tf-test-git.git", + }, + { + "bitbucket.org/hashicorp/tf-test-hg", + "hg::https://bitbucket.org/hashicorp/tf-test-hg", + }, + } + + pwd := "/pwd" + f := new(BitBucketDetector) + for i, tc := range cases { + output, ok, err := f.Detect(tc.Input, pwd) + if err != nil { + t.Fatalf("err: %s", err) + } + if !ok { + t.Fatal("not ok") + } + + if output != tc.Output { + t.Fatalf("%d: bad: %#v", i, output) + } + } +} diff --git a/config/module/detect_file.go b/config/module/detect_file.go new file mode 100644 index 0000000000..ac9ad8e678 --- /dev/null +++ b/config/module/detect_file.go @@ -0,0 +1,33 @@ +package module + +import ( + "fmt" + "path/filepath" +) + +// FileDetector implements Detector to detect file paths. +type FileDetector struct{} + +func (d *FileDetector) Detect(src, pwd string) (string, bool, error) { + if len(src) == 0 { + return "", false, nil + } + + // Make sure we're using "/" even on Windows. URLs are "/"-based. + src = filepath.ToSlash(src) + if !filepath.IsAbs(src) { + if pwd == "" { + return "", true, fmt.Errorf( + "relative paths require a module with a pwd") + } + + src = filepath.Join(pwd, src) + } + + // Make sure that we don't start with "/" since we add that below + if src[0] == '/' { + src = src[1:] + } + + return fmt.Sprintf("file:///%s", src), true, nil +} diff --git a/config/module/detect_file_test.go b/config/module/detect_file_test.go new file mode 100644 index 0000000000..02a6ccec28 --- /dev/null +++ b/config/module/detect_file_test.go @@ -0,0 +1,62 @@ +package module + +import ( + "testing" +) + +func TestFileDetector(t *testing.T) { + cases := []struct { + Input string + Output string + }{ + {"./foo", "file:///pwd/foo"}, + {"./foo?foo=bar", "file:///pwd/foo?foo=bar"}, + {"foo", "file:///pwd/foo"}, + {"/foo", "file:///foo"}, + {"/foo?bar=baz", "file:///foo?bar=baz"}, + } + + pwd := "/pwd" + f := new(FileDetector) + for i, tc := range cases { + output, ok, err := f.Detect(tc.Input, pwd) + if err != nil { + t.Fatalf("err: %s", err) + } + if !ok { + t.Fatal("not ok") + } + + if output != tc.Output { + t.Fatalf("%d: bad: %#v", i, output) + } + } +} + +func TestFileDetector_noPwd(t *testing.T) { + cases := []struct { + Input string + Output string + Err bool + }{ + {"./foo", "", true}, + {"foo", "", true}, + {"/foo", "file:///foo", false}, + } + + pwd := "" + f := new(FileDetector) + for i, tc := range cases { + output, ok, err := f.Detect(tc.Input, pwd) + if (err != nil) != tc.Err { + t.Fatalf("%d: err: %s", i, err) + } + if !ok { + t.Fatal("not ok") + } + + if output != tc.Output { + t.Fatalf("%d: bad: %#v", i, output) + } + } +} diff --git a/config/module/detect_github.go b/config/module/detect_github.go new file mode 100644 index 0000000000..e4854cf424 --- /dev/null +++ b/config/module/detect_github.go @@ -0,0 +1,63 @@ +package module + +import ( + "fmt" + "net/url" + "strings" +) + +// GitHubDetector implements Detector to detect GitHub URLs and turn +// them into URLs that the Git Getter can understand. +type GitHubDetector struct{} + +func (d *GitHubDetector) Detect(src, _ string) (string, bool, error) { + if len(src) == 0 { + return "", false, nil + } + + if strings.HasPrefix(src, "github.com/") { + return d.detectHTTP(src) + } else if strings.HasPrefix(src, "git@github.com:") { + return d.detectSSH(src) + } + + return "", false, nil +} + +func (d *GitHubDetector) detectHTTP(src string) (string, bool, error) { + urlStr := fmt.Sprintf("https://%s", src) + url, err := url.Parse(urlStr) + if err != nil { + return "", true, fmt.Errorf("error parsing GitHub URL: %s", err) + } + + if !strings.HasSuffix(url.Path, ".git") { + url.Path += ".git" + } + + return "git::" + url.String(), true, nil +} + +func (d *GitHubDetector) detectSSH(src string) (string, bool, error) { + idx := strings.Index(src, ":") + qidx := strings.Index(src, "?") + if qidx == -1 { + qidx = len(src) + } + + var u url.URL + u.Scheme = "ssh" + u.User = url.User("git") + u.Host = "github.com" + u.Path = src[idx+1:qidx] + if qidx < len(src) { + q, err := url.ParseQuery(src[qidx+1:]) + if err != nil { + return "", true, fmt.Errorf("error parsing GitHub SSH URL: %s", err) + } + + u.RawQuery = q.Encode() + } + + return "git::"+u.String(), true, nil +} diff --git a/config/module/detect_github_test.go b/config/module/detect_github_test.go new file mode 100644 index 0000000000..37fac84c87 --- /dev/null +++ b/config/module/detect_github_test.go @@ -0,0 +1,47 @@ +package module + +import ( + "testing" +) + +func TestGitHubDetector(t *testing.T) { + cases := []struct { + Input string + Output string + }{ + // HTTP + {"github.com/hashicorp/foo", "git::https://github.com/hashicorp/foo.git"}, + {"github.com/hashicorp/foo.git", "git::https://github.com/hashicorp/foo.git"}, + { + "github.com/hashicorp/foo?foo=bar", + "git::https://github.com/hashicorp/foo.git?foo=bar", + }, + { + "github.com/hashicorp/foo.git?foo=bar", + "git::https://github.com/hashicorp/foo.git?foo=bar", + }, + + // SSH + {"git@github.com:hashicorp/foo.git", "git::ssh://git@github.com/hashicorp/foo.git"}, + { + "git@github.com:hashicorp/foo.git?foo=bar", + "git::ssh://git@github.com/hashicorp/foo.git?foo=bar", + }, + } + + pwd := "/pwd" + f := new(GitHubDetector) + for i, tc := range cases { + output, ok, err := f.Detect(tc.Input, pwd) + if err != nil { + t.Fatalf("err: %s", err) + } + if !ok { + t.Fatal("not ok") + } + + if output != tc.Output { + t.Fatalf("%d: bad: %#v", i, output) + } + } +} diff --git a/config/module/detect_test.go b/config/module/detect_test.go new file mode 100644 index 0000000000..8f62f6618b --- /dev/null +++ b/config/module/detect_test.go @@ -0,0 +1,28 @@ +package module + +import ( + "testing" +) + +func TestDetect(t *testing.T) { + cases := []struct { + Input string + Pwd string + Output string + Err bool + }{ + {"./foo", "/foo", "file:///foo/foo", false}, + {"git::./foo", "/foo", "git::file:///foo/foo", false}, + {"git::github.com/hashicorp/foo", "", "git::https://github.com/hashicorp/foo.git", false}, + } + + for i, tc := range cases { + output, err := Detect(tc.Input, tc.Pwd) + if (err != nil) != tc.Err { + t.Fatalf("%d: bad err: %s", i, err) + } + if output != tc.Output { + t.Fatalf("%d: bad output: %s", i, output) + } + } +} diff --git a/config/module/folder_storage.go b/config/module/folder_storage.go new file mode 100644 index 0000000000..dfb79748af --- /dev/null +++ b/config/module/folder_storage.go @@ -0,0 +1,65 @@ +package module + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "os" + "path/filepath" +) + +// FolderStorage is an implementation of the Storage interface that manages +// modules on the disk. +type FolderStorage struct { + // StorageDir is the directory where the modules will be stored. + StorageDir string +} + +// Dir implements Storage.Dir +func (s *FolderStorage) Dir(source string) (d string, e bool, err error) { + d = s.dir(source) + _, err = os.Stat(d) + if err == nil { + // Directory exists + e = true + return + } + if os.IsNotExist(err) { + // Directory doesn't exist + d = "" + e = false + err = nil + return + } + + // An error + d = "" + e = false + return +} + +// Get implements Storage.Get +func (s *FolderStorage) Get(source string, update bool) error { + dir := s.dir(source) + if !update { + if _, err := os.Stat(dir); err == nil { + // If the directory already exists, then we're done since + // we're not updating. + return nil + } else if !os.IsNotExist(err) { + // If the error we got wasn't a file-not-exist error, then + // something went wrong and we should report it. + return fmt.Errorf("Error reading module directory: %s", err) + } + } + + // Get the source. This always forces an update. + return Get(dir, source) +} + +// dir returns the directory name internally that we'll use to map to +// internally. +func (s *FolderStorage) dir(source string) string { + sum := md5.Sum([]byte(source)) + return filepath.Join(s.StorageDir, hex.EncodeToString(sum[:])) +} diff --git a/config/module/folder_storage_test.go b/config/module/folder_storage_test.go new file mode 100644 index 0000000000..4ffaac2bb1 --- /dev/null +++ b/config/module/folder_storage_test.go @@ -0,0 +1,46 @@ +package module + +import ( + "os" + "path/filepath" + "testing" +) + +func TestFolderStorage_impl(t *testing.T) { + var _ Storage = new(FolderStorage) +} + +func TestFolderStorage(t *testing.T) { + s := &FolderStorage{StorageDir: tempDir(t)} + + module := testModule("basic") + + // A module shouldn't exist at first... + _, ok, err := s.Dir(module) + if err != nil { + t.Fatalf("err: %s", err) + } + if ok { + t.Fatal("should not exist") + } + + // We can get it + err = s.Get(module, false) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Now the module exists + dir, ok, err := s.Dir(module) + if err != nil { + t.Fatalf("err: %s", err) + } + if !ok { + t.Fatal("should exist") + } + + mainPath := filepath.Join(dir, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/config/module/get.go b/config/module/get.go new file mode 100644 index 0000000000..89a1436049 --- /dev/null +++ b/config/module/get.go @@ -0,0 +1,109 @@ +package module + +import ( + "bytes" + "fmt" + "net/url" + "os/exec" + "regexp" + "syscall" +) + +// Getter defines the interface that schemes must implement to download +// and update modules. +type Getter interface { + // Get downloads the given URL into the given directory. This always + // assumes that we're updating and gets the latest version that it can. + // + // The directory may already exist (if we're updating). If it is in a + // format that isn't understood, an error should be returned. Get shouldn't + // simply nuke the directory. + Get(string, *url.URL) error +} + +// Getters is the mapping of scheme to the Getter implementation that will +// be used to get a dependency. +var Getters map[string]Getter + +// forcedRegexp is the regular expression that finds forced getters. This +// syntax is schema::url, example: git::https://foo.com +var forcedRegexp = regexp.MustCompile(`^([A-Za-z]+)::(.+)$`) + +func init() { + httpGetter := new(HttpGetter) + + Getters = map[string]Getter{ + "file": new(FileGetter), + "git": new(GitGetter), + "hg": new(HgGetter), + "http": httpGetter, + "https": httpGetter, + } +} + +// Get downloads the module specified by src into the folder specified by +// dst. If dst already exists, Get will attempt to update it. +// +// src is a URL, whereas dst is always just a file path to a folder. This +// folder doesn't need to exist. It will be created if it doesn't exist. +func Get(dst, src string) error { + var force string + force, src = getForcedGetter(src) + + u, err := url.Parse(src) + if err != nil { + return err + } + if force == "" { + force = u.Scheme + } + + g, ok := Getters[force] + if !ok { + return fmt.Errorf( + "module download not supported for scheme '%s'", force) + } + + err = g.Get(dst, u) + if err != nil { + err = fmt.Errorf("error downloading module '%s': %s", src, err) + } + + return err +} + +// getRunCommand is a helper that will run a command and capture the output +// in the case an error happens. +func getRunCommand(cmd *exec.Cmd) error { + var buf bytes.Buffer + cmd.Stdout = &buf + cmd.Stderr = &buf + err := cmd.Run() + if err == nil { + return nil + } + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + return fmt.Errorf( + "%s exited with %d: %s", + cmd.Path, + status.ExitStatus(), + buf.String()) + } + } + + return fmt.Errorf("error running %s: %s", cmd.Path, buf.String()) +} + +// getForcedGetter takes a source and returns the tuple of the forced +// getter and the raw URL (without the force syntax). +func getForcedGetter(src string) (string, string) { + var forced string + if ms := forcedRegexp.FindStringSubmatch(src); ms != nil { + forced = ms[1] + src = ms[2] + } + + return forced, src +} diff --git a/config/module/get_file.go b/config/module/get_file.go new file mode 100644 index 0000000000..73cb858341 --- /dev/null +++ b/config/module/get_file.go @@ -0,0 +1,46 @@ +package module + +import ( + "fmt" + "net/url" + "os" + "path/filepath" +) + +// FileGetter is a Getter implementation that will download a module from +// a file scheme. +type FileGetter struct{} + +func (g *FileGetter) Get(dst string, u *url.URL) error { + // The source path must exist and be a directory to be usable. + if fi, err := os.Stat(u.Path); err != nil { + return fmt.Errorf("source path error: %s", err) + } else if !fi.IsDir() { + return fmt.Errorf("source path must be a directory") + } + + fi, err := os.Lstat(dst) + if err != nil && !os.IsNotExist(err) { + return err + } + + // If the destination already exists, it must be a symlink + if err == nil { + mode := fi.Mode() + if mode&os.ModeSymlink == 0 { + return fmt.Errorf("destination exists and is not a symlink") + } + + // Remove the destination + if err := os.Remove(dst); err != nil { + return err + } + } + + // Create all the parent directories + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return err + } + + return os.Symlink(u.Path, dst) +} diff --git a/config/module/get_file_test.go b/config/module/get_file_test.go new file mode 100644 index 0000000000..7cc69bccb9 --- /dev/null +++ b/config/module/get_file_test.go @@ -0,0 +1,104 @@ +package module + +import ( + "os" + "path/filepath" + "testing" +) + +func TestFileGetter_impl(t *testing.T) { + var _ Getter = new(FileGetter) +} + +func TestFileGetter(t *testing.T) { + g := new(FileGetter) + dst := tempDir(t) + + // With a dir that doesn't exist + if err := g.Get(dst, testModuleURL("basic")); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the destination folder is a symlink + fi, err := os.Lstat(dst) + if err != nil { + t.Fatalf("err: %s", err) + } + if fi.Mode()&os.ModeSymlink == 0 { + t.Fatal("destination is not a symlink") + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestFileGetter_sourceFile(t *testing.T) { + g := new(FileGetter) + dst := tempDir(t) + + // With a source URL that is a path to a file + u := testModuleURL("basic") + u.Path += "/main.tf" + if err := g.Get(dst, u); err == nil { + t.Fatal("should error") + } +} + +func TestFileGetter_sourceNoExist(t *testing.T) { + g := new(FileGetter) + dst := tempDir(t) + + // With a source URL that doesn't exist + u := testModuleURL("basic") + u.Path += "/main" + if err := g.Get(dst, u); err == nil { + t.Fatal("should error") + } +} + +func TestFileGetter_dir(t *testing.T) { + g := new(FileGetter) + dst := tempDir(t) + + if err := os.MkdirAll(dst, 0755); err != nil { + t.Fatalf("err: %s", err) + } + + // With a dir that exists that isn't a symlink + if err := g.Get(dst, testModuleURL("basic")); err == nil { + t.Fatal("should error") + } +} + +func TestFileGetter_dirSymlink(t *testing.T) { + g := new(FileGetter) + dst := tempDir(t) + dst2 := tempDir(t) + + // Make parents + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + t.Fatalf("err: %s", err) + } + if err := os.MkdirAll(dst2, 0755); err != nil { + t.Fatalf("err: %s", err) + } + + // Make a symlink + if err := os.Symlink(dst2, dst); err != nil { + t.Fatalf("err: %s") + } + + // With a dir that exists that isn't a symlink + if err := g.Get(dst, testModuleURL("basic")); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/config/module/get_git.go b/config/module/get_git.go new file mode 100644 index 0000000000..5ab27ba0be --- /dev/null +++ b/config/module/get_git.go @@ -0,0 +1,74 @@ +package module + +import ( + "fmt" + "net/url" + "os" + "os/exec" +) + +// GitGetter is a Getter implementation that will download a module from +// a git repository. +type GitGetter struct{} + +func (g *GitGetter) Get(dst string, u *url.URL) error { + if _, err := exec.LookPath("git"); err != nil { + return fmt.Errorf("git must be available and on the PATH") + } + + // Extract some query parameters we use + var ref string + q := u.Query() + if len(q) > 0 { + ref = q.Get("ref") + q.Del("ref") + + // Copy the URL + var newU url.URL = *u + u = &newU + u.RawQuery = q.Encode() + } + + // First: clone or update the repository + _, err := os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return err + } + if err == nil { + err = g.update(dst, u) + } else { + err = g.clone(dst, u) + } + if err != nil { + return err + } + + // Next: check out the proper tag/branch if it is specified, and checkout + if ref == "" { + return nil + } + + return g.checkout(dst, ref) +} + +func (g *GitGetter) checkout(dst string, ref string) error { + cmd := exec.Command("git", "checkout", ref) + cmd.Dir = dst + return getRunCommand(cmd) +} + +func (g *GitGetter) clone(dst string, u *url.URL) error { + cmd := exec.Command("git", "clone", u.String(), dst) + return getRunCommand(cmd) +} + +func (g *GitGetter) update(dst string, u *url.URL) error { + // We have to be on a branch to pull + if err := g.checkout(dst, "master"); err != nil { + return err + } + + cmd := exec.Command("git", "pull", "--ff-only") + cmd.Dir = dst + return getRunCommand(cmd) +} diff --git a/config/module/get_git_test.go b/config/module/get_git_test.go new file mode 100644 index 0000000000..3885ff8e79 --- /dev/null +++ b/config/module/get_git_test.go @@ -0,0 +1,143 @@ +package module + +import ( + "os" + "os/exec" + "path/filepath" + "testing" +) + +var testHasGit bool + +func init() { + if _, err := exec.LookPath("git"); err == nil { + testHasGit = true + } +} + +func TestGitGetter_impl(t *testing.T) { + var _ Getter = new(GitGetter) +} + +func TestGitGetter(t *testing.T) { + if !testHasGit { + t.Log("git not found, skipping") + t.Skip() + } + + g := new(GitGetter) + dst := tempDir(t) + + // Git doesn't allow nested ".git" directories so we do some hackiness + // here to get around that... + moduleDir := filepath.Join(fixtureDir, "basic-git") + oldName := filepath.Join(moduleDir, "DOTgit") + newName := filepath.Join(moduleDir, ".git") + if err := os.Rename(oldName, newName); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Rename(newName, oldName) + + // With a dir that doesn't exist + if err := g.Get(dst, testModuleURL("basic-git")); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestGitGetter_branch(t *testing.T) { + if !testHasGit { + t.Log("git not found, skipping") + t.Skip() + } + + g := new(GitGetter) + dst := tempDir(t) + + // Git doesn't allow nested ".git" directories so we do some hackiness + // here to get around that... + moduleDir := filepath.Join(fixtureDir, "basic-git") + oldName := filepath.Join(moduleDir, "DOTgit") + newName := filepath.Join(moduleDir, ".git") + if err := os.Rename(oldName, newName); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Rename(newName, oldName) + + url := testModuleURL("basic-git") + q := url.Query() + q.Add("ref", "test-branch") + url.RawQuery = q.Encode() + + if err := g.Get(dst, url); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main_branch.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } + + // Get again should work + if err := g.Get(dst, url); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath = filepath.Join(dst, "main_branch.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestGitGetter_tag(t *testing.T) { + if !testHasGit { + t.Log("git not found, skipping") + t.Skip() + } + + g := new(GitGetter) + dst := tempDir(t) + + // Git doesn't allow nested ".git" directories so we do some hackiness + // here to get around that... + moduleDir := filepath.Join(fixtureDir, "basic-git") + oldName := filepath.Join(moduleDir, "DOTgit") + newName := filepath.Join(moduleDir, ".git") + if err := os.Rename(oldName, newName); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Rename(newName, oldName) + + url := testModuleURL("basic-git") + q := url.Query() + q.Add("ref", "v1.0") + url.RawQuery = q.Encode() + + if err := g.Get(dst, url); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main_tag1.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } + + // Get again should work + if err := g.Get(dst, url); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath = filepath.Join(dst, "main_tag1.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/config/module/get_hg.go b/config/module/get_hg.go new file mode 100644 index 0000000000..19e4abd5ab --- /dev/null +++ b/config/module/get_hg.go @@ -0,0 +1,69 @@ +package module + +import ( + "fmt" + "net/url" + "os" + "os/exec" +) + +// HgGetter is a Getter implementation that will download a module from +// a Mercurial repository. +type HgGetter struct{} + +func (g *HgGetter) Get(dst string, u *url.URL) error { + if _, err := exec.LookPath("hg"); err != nil { + return fmt.Errorf("hg must be available and on the PATH") + } + + // Extract some query parameters we use + var rev string + q := u.Query() + if len(q) > 0 { + rev = q.Get("rev") + q.Del("rev") + + // Copy the URL + var newU url.URL = *u + u = &newU + u.RawQuery = q.Encode() + } + + _, err := os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return err + } + if err != nil { + if err := g.clone(dst, u); err != nil { + return err + } + } + + if err:= g.pull(dst, u); err != nil { + return err + } + + return g.update(dst, u, rev) +} + +func (g *HgGetter) clone(dst string, u *url.URL) error { + cmd := exec.Command("hg", "clone", "-U", u.String(), dst) + return getRunCommand(cmd) +} + +func (g *HgGetter) pull(dst string, u *url.URL) error { + cmd := exec.Command("hg", "pull") + cmd.Dir = dst + return getRunCommand(cmd) +} + +func (g *HgGetter) update(dst string, u *url.URL, rev string) error { + args := []string{"update"} + if rev != "" { + args = append(args, rev) + } + + cmd := exec.Command("hg", args...) + cmd.Dir = dst + return getRunCommand(cmd) +} diff --git a/config/module/get_hg_test.go b/config/module/get_hg_test.go new file mode 100644 index 0000000000..d7125bde21 --- /dev/null +++ b/config/module/get_hg_test.go @@ -0,0 +1,81 @@ +package module + +import ( + "os" + "os/exec" + "path/filepath" + "testing" +) + +var testHasHg bool + +func init() { + if _, err := exec.LookPath("hg"); err == nil { + testHasHg = true + } +} + +func TestHgGetter_impl(t *testing.T) { + var _ Getter = new(HgGetter) +} + +func TestHgGetter(t *testing.T) { + t.Parallel() + + if !testHasHg { + t.Log("hg not found, skipping") + t.Skip() + } + + g := new(HgGetter) + dst := tempDir(t) + + // With a dir that doesn't exist + if err := g.Get(dst, testModuleURL("basic-hg")); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestHgGetter_branch(t *testing.T) { + t.Parallel() + + if !testHasHg { + t.Log("hg not found, skipping") + t.Skip() + } + + g := new(HgGetter) + dst := tempDir(t) + + url := testModuleURL("basic-hg") + q := url.Query() + q.Add("rev", "test-branch") + url.RawQuery = q.Encode() + + if err := g.Get(dst, url); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main_branch.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } + + // Get again should work + if err := g.Get(dst, url); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath = filepath.Join(dst, "main_branch.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/config/module/get_http.go b/config/module/get_http.go new file mode 100644 index 0000000000..7aa7d9abfd --- /dev/null +++ b/config/module/get_http.go @@ -0,0 +1,128 @@ +package module + +import ( + "encoding/xml" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// HttpGetter is a Getter implementation that will download a module from +// an HTTP endpoint. The protocol for downloading a module from an HTTP +// endpoing is as follows: +// +// An HTTP GET request is made to the URL with the additional GET parameter +// "terraform-get=1". This lets you handle that scenario specially if you +// wish. The response must be a 2xx. +// +// First, a header is looked for "X-Terraform-Get" which should contain +// a source URL to download. +// +// If the header is not present, then a meta tag is searched for named +// "terraform-get" and the content should be a source URL. +// +// The source URL, whether from the header or meta tag, must be a fully +// formed URL. The shorthand syntax of "github.com/foo/bar" or relative +// paths are not allowed. +type HttpGetter struct{} + +func (g *HttpGetter) Get(dst string, u *url.URL) error { + // Copy the URL so we can modify it + var newU url.URL = *u + u = &newU + + // Add terraform-get to the parameter. + q := u.Query() + q.Add("terraform-get", "1") + u.RawQuery = q.Encode() + + // Get the URL + resp, err := http.Get(u.String()) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("bad response code: %d", resp.StatusCode) + } + + // Extract the source URL + var source string + if v := resp.Header.Get("X-Terraform-Get"); v != "" { + source = v + } else { + source, err = g.parseMeta(resp.Body) + if err != nil { + return err + } + } + if source == "" { + return fmt.Errorf("no source URL was returned") + } + + // Get it! + return Get(dst, source) +} + +// parseMeta looks for the first meta tag in the given reader that +// will give us the source URL. +func (g *HttpGetter) parseMeta(r io.Reader) (string, error) { + d := xml.NewDecoder(r) + d.CharsetReader = charsetReader + d.Strict = false + var err error + var t xml.Token + for { + t, err = d.Token() + if err != nil { + if err == io.EOF { + err = nil + } + return "", err + } + if e, ok := t.(xml.StartElement); ok && strings.EqualFold(e.Name.Local, "body") { + return "", nil + } + if e, ok := t.(xml.EndElement); ok && strings.EqualFold(e.Name.Local, "head") { + return "", nil + } + e, ok := t.(xml.StartElement) + if !ok || !strings.EqualFold(e.Name.Local, "meta") { + continue + } + if attrValue(e.Attr, "name") != "terraform-get" { + continue + } + if f := attrValue(e.Attr, "content"); f != "" { + return f, nil + } + } +} + +// attrValue returns the attribute value for the case-insensitive key +// `name', or the empty string if nothing is found. +func attrValue(attrs []xml.Attr, name string) string { + for _, a := range attrs { + if strings.EqualFold(a.Name.Local, name) { + return a.Value + } + } + return "" +} + +// charsetReader returns a reader for the given charset. Currently +// it only supports UTF-8 and ASCII. Otherwise, it returns a meaningful +// error which is printed by go get, so the user can find why the package +// wasn't downloaded if the encoding is not supported. Note that, in +// order to reduce potential errors, ASCII is treated as UTF-8 (i.e. characters +// greater than 0x7f are not rejected). +func charsetReader(charset string, input io.Reader) (io.Reader, error) { + switch strings.ToLower(charset) { + case "ascii": + return input, nil + default: + return nil, fmt.Errorf("can't decode XML document using charset %q", charset) + } +} diff --git a/config/module/get_http_test.go b/config/module/get_http_test.go new file mode 100644 index 0000000000..8fa61d2e49 --- /dev/null +++ b/config/module/get_http_test.go @@ -0,0 +1,126 @@ +package module + +import ( + "fmt" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "testing" +) + +func TestHttpGetter_impl(t *testing.T) { + var _ Getter = new(HttpGetter) +} + +func TestHttpGetter_header(t *testing.T) { + ln := testHttpServer(t) + defer ln.Close() + + g := new(HttpGetter) + dst := tempDir(t) + + var u url.URL + u.Scheme = "http" + u.Host = ln.Addr().String() + u.Path = "/header" + + // Get it! + if err := g.Get(dst, &u); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestHttpGetter_meta(t *testing.T) { + ln := testHttpServer(t) + defer ln.Close() + + g := new(HttpGetter) + dst := tempDir(t) + + var u url.URL + u.Scheme = "http" + u.Host = ln.Addr().String() + u.Path = "/meta" + + // Get it! + if err := g.Get(dst, &u); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestHttpGetter_none(t *testing.T) { + ln := testHttpServer(t) + defer ln.Close() + + g := new(HttpGetter) + dst := tempDir(t) + + var u url.URL + u.Scheme = "http" + u.Host = ln.Addr().String() + u.Path = "/none" + + // Get it! + if err := g.Get(dst, &u); err == nil { + t.Fatal("should error") + } +} + +func testHttpServer(t *testing.T) net.Listener { + ln, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatalf("err: %s", err) + } + + mux := http.NewServeMux() + mux.HandleFunc("/header", testHttpHandlerHeader) + mux.HandleFunc("/meta", testHttpHandlerMeta) + + var server http.Server + server.Handler = mux + go server.Serve(ln) + + return ln +} + +func testHttpHandlerHeader(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-Terraform-Get", testModuleURL("basic").String()) + w.WriteHeader(200) +} + +func testHttpHandlerMeta(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(fmt.Sprintf(testHttpMetaStr, testModuleURL("basic").String()))) +} + +func testHttpHandlerNone(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(testHttpNoneStr)) +} + +const testHttpMetaStr = ` + + + + + +` + +const testHttpNoneStr = ` + + + + +` diff --git a/config/module/get_test.go b/config/module/get_test.go new file mode 100644 index 0000000000..742e9d7947 --- /dev/null +++ b/config/module/get_test.go @@ -0,0 +1,47 @@ +package module + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestGet_badSchema(t *testing.T) { + dst := tempDir(t) + u := testModule("basic") + u = strings.Replace(u, "file", "nope", -1) + + if err := Get(dst, u); err == nil { + t.Fatal("should error") + } +} + +func TestGet_file(t *testing.T) { + dst := tempDir(t) + u := testModule("basic") + + if err := Get(dst, u); err != nil { + t.Fatalf("err: %s", err) + } + + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestGet_fileForced(t *testing.T) { + dst := tempDir(t) + u := testModule("basic") + u = "file::"+u + + if err := Get(dst, u); err != nil { + t.Fatalf("err: %s", err) + } + + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/config/module/module.go b/config/module/module.go new file mode 100644 index 0000000000..f8649f6e9d --- /dev/null +++ b/config/module/module.go @@ -0,0 +1,7 @@ +package module + +// Module represents the metadata for a single module. +type Module struct { + Name string + Source string +} diff --git a/config/module/module_test.go b/config/module/module_test.go new file mode 100644 index 0000000000..bd72f08629 --- /dev/null +++ b/config/module/module_test.go @@ -0,0 +1,60 @@ +package module + +import ( + "io/ioutil" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/hashicorp/terraform/config" +) + +const fixtureDir = "./test-fixtures" + +func tempDir(t *testing.T) string { + dir, err := ioutil.TempDir("", "tf") + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.RemoveAll(dir); err != nil { + t.Fatalf("err: %s", err) + } + + return dir +} + +func testConfig(t *testing.T, n string) *config.Config { + c, err := config.LoadDir(filepath.Join(fixtureDir, n)) + if err != nil { + t.Fatalf("err: %s", err) + } + + return c +} + +func testModule(n string) string { + p := filepath.Join(fixtureDir, n) + p, err := filepath.Abs(p) + if err != nil { + panic(err) + } + + var url url.URL + url.Scheme = "file" + url.Path = p + return url.String() +} + +func testModuleURL(n string) *url.URL { + u, err := url.Parse(testModule(n)) + if err != nil { + panic(err) + } + + return u +} + +func testStorage(t *testing.T) Storage { + return &FolderStorage{StorageDir: tempDir(t)} +} diff --git a/config/module/storage.go b/config/module/storage.go new file mode 100644 index 0000000000..14b5181e54 --- /dev/null +++ b/config/module/storage.go @@ -0,0 +1,13 @@ +package module + +// Storage is an interface that knows how to lookup downloaded modules +// as well as download and update modules from their sources into the +// proper location. +type Storage interface { + // Dir returns the directory on local disk where the modulue source + // can be loaded from. + Dir(string) (string, bool, error) + + // Get will download and optionally update the given module. + Get(string, bool) error +} diff --git a/config/module/test-fixtures/basic-git/DOTgit/COMMIT_EDITMSG b/config/module/test-fixtures/basic-git/DOTgit/COMMIT_EDITMSG new file mode 100644 index 0000000000..d13fed6c9b --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/COMMIT_EDITMSG @@ -0,0 +1,7 @@ +Branch +# Please enter the commit message for your changes. Lines starting +# with '#' will be ignored, and an empty message aborts the commit. +# On branch test-branch +# Changes to be committed: +# new file: main_branch.tf +# diff --git a/config/module/test-fixtures/basic-git/DOTgit/HEAD b/config/module/test-fixtures/basic-git/DOTgit/HEAD new file mode 100644 index 0000000000..cb089cd89a --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/config/module/test-fixtures/basic-git/DOTgit/config b/config/module/test-fixtures/basic-git/DOTgit/config new file mode 100644 index 0000000000..6c9406b7d9 --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/config @@ -0,0 +1,7 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true diff --git a/config/module/test-fixtures/basic-git/DOTgit/description b/config/module/test-fixtures/basic-git/DOTgit/description new file mode 100644 index 0000000000..498b267a8c --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/config/module/test-fixtures/basic-git/DOTgit/hooks/applypatch-msg.sample b/config/module/test-fixtures/basic-git/DOTgit/hooks/applypatch-msg.sample new file mode 100755 index 0000000000..8b2a2fe84f --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +test -x "$GIT_DIR/hooks/commit-msg" && + exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"} +: diff --git a/config/module/test-fixtures/basic-git/DOTgit/hooks/commit-msg.sample b/config/module/test-fixtures/basic-git/DOTgit/hooks/commit-msg.sample new file mode 100755 index 0000000000..b58d1184a9 --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/config/module/test-fixtures/basic-git/DOTgit/hooks/post-update.sample b/config/module/test-fixtures/basic-git/DOTgit/hooks/post-update.sample new file mode 100755 index 0000000000..ec17ec1939 --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/config/module/test-fixtures/basic-git/DOTgit/hooks/pre-applypatch.sample b/config/module/test-fixtures/basic-git/DOTgit/hooks/pre-applypatch.sample new file mode 100755 index 0000000000..b1f187c2e9 --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"} +: diff --git a/config/module/test-fixtures/basic-git/DOTgit/hooks/pre-commit.sample b/config/module/test-fixtures/basic-git/DOTgit/hooks/pre-commit.sample new file mode 100755 index 0000000000..68d62d5446 --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/config/module/test-fixtures/basic-git/DOTgit/hooks/pre-push.sample b/config/module/test-fixtures/basic-git/DOTgit/hooks/pre-push.sample new file mode 100755 index 0000000000..1f3bcebfd7 --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/hooks/pre-push.sample @@ -0,0 +1,54 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +z40=0000000000000000000000000000000000000000 + +IFS=' ' +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" = $z40 ] + then + # Handle delete + : + else + if [ "$remote_sha" = $z40 ] + then + # New branch, examine all commits + range="$local_sha" + else + # Update to existing branch, examine new commits + range="$remote_sha..$local_sha" + fi + + # Check for WIP commit + commit=`git rev-list -n 1 --grep '^WIP' "$range"` + if [ -n "$commit" ] + then + echo "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/config/module/test-fixtures/basic-git/DOTgit/hooks/pre-rebase.sample b/config/module/test-fixtures/basic-git/DOTgit/hooks/pre-rebase.sample new file mode 100755 index 0000000000..9773ed4cb2 --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up-to-date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +exit 0 + +################################################################ + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". diff --git a/config/module/test-fixtures/basic-git/DOTgit/hooks/prepare-commit-msg.sample b/config/module/test-fixtures/basic-git/DOTgit/hooks/prepare-commit-msg.sample new file mode 100755 index 0000000000..f093a02ec4 --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/hooks/prepare-commit-msg.sample @@ -0,0 +1,36 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first comments out the +# "Conflicts:" part of a merge commit. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +case "$2,$3" in + merge,) + /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; + +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$1" ;; + + *) ;; +esac + +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" diff --git a/config/module/test-fixtures/basic-git/DOTgit/hooks/update.sample b/config/module/test-fixtures/basic-git/DOTgit/hooks/update.sample new file mode 100755 index 0000000000..d84758373d --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to blocks unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --bool hooks.allowunannotated) +allowdeletebranch=$(git config --bool hooks.allowdeletebranch) +denycreatebranch=$(git config --bool hooks.denycreatebranch) +allowdeletetag=$(git config --bool hooks.allowdeletetag) +allowmodifytag=$(git config --bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero="0000000000000000000000000000000000000000" +if [ "$newrev" = "$zero" ]; then + newrev_type=delete +else + newrev_type=$(git cat-file -t $newrev) +fi + +case "$refname","$newrev_type" in + refs/tags/*,commit) + # un-annotated tag + short_refname=${refname##refs/tags/} + if [ "$allowunannotated" != "true" ]; then + echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/config/module/test-fixtures/basic-git/DOTgit/index b/config/module/test-fixtures/basic-git/DOTgit/index new file mode 100644 index 0000000000..071f9d4396 Binary files /dev/null and b/config/module/test-fixtures/basic-git/DOTgit/index differ diff --git a/config/module/test-fixtures/basic-git/DOTgit/info/exclude b/config/module/test-fixtures/basic-git/DOTgit/info/exclude new file mode 100644 index 0000000000..a5196d1be8 --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/config/module/test-fixtures/basic-git/DOTgit/logs/HEAD b/config/module/test-fixtures/basic-git/DOTgit/logs/HEAD new file mode 100644 index 0000000000..40709bc8e1 --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/logs/HEAD @@ -0,0 +1,6 @@ +0000000000000000000000000000000000000000 497bc37401eb3c9b11865b1768725b64066eccee Mitchell Hashimoto 1410850637 -0700 commit (initial): A commit +497bc37401eb3c9b11865b1768725b64066eccee 243f0fc5c4e586d1a3daa54c981b6f34e9ab1085 Mitchell Hashimoto 1410886526 -0700 commit: tag1 +243f0fc5c4e586d1a3daa54c981b6f34e9ab1085 1f31e97f053caeb5d6b7bffa3faf82941c99efa2 Mitchell Hashimoto 1410886536 -0700 commit: remove tag1 +1f31e97f053caeb5d6b7bffa3faf82941c99efa2 1f31e97f053caeb5d6b7bffa3faf82941c99efa2 Mitchell Hashimoto 1410886909 -0700 checkout: moving from master to test-branch +1f31e97f053caeb5d6b7bffa3faf82941c99efa2 7b7614f8759ac8b5e4b02be65ad8e2667be6dd87 Mitchell Hashimoto 1410886913 -0700 commit: Branch +7b7614f8759ac8b5e4b02be65ad8e2667be6dd87 1f31e97f053caeb5d6b7bffa3faf82941c99efa2 Mitchell Hashimoto 1410886916 -0700 checkout: moving from test-branch to master diff --git a/config/module/test-fixtures/basic-git/DOTgit/logs/refs/heads/master b/config/module/test-fixtures/basic-git/DOTgit/logs/refs/heads/master new file mode 100644 index 0000000000..396932ba1d --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/logs/refs/heads/master @@ -0,0 +1,3 @@ +0000000000000000000000000000000000000000 497bc37401eb3c9b11865b1768725b64066eccee Mitchell Hashimoto 1410850637 -0700 commit (initial): A commit +497bc37401eb3c9b11865b1768725b64066eccee 243f0fc5c4e586d1a3daa54c981b6f34e9ab1085 Mitchell Hashimoto 1410886526 -0700 commit: tag1 +243f0fc5c4e586d1a3daa54c981b6f34e9ab1085 1f31e97f053caeb5d6b7bffa3faf82941c99efa2 Mitchell Hashimoto 1410886536 -0700 commit: remove tag1 diff --git a/config/module/test-fixtures/basic-git/DOTgit/logs/refs/heads/test-branch b/config/module/test-fixtures/basic-git/DOTgit/logs/refs/heads/test-branch new file mode 100644 index 0000000000..937067a2a1 --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/logs/refs/heads/test-branch @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 1f31e97f053caeb5d6b7bffa3faf82941c99efa2 Mitchell Hashimoto 1410886909 -0700 branch: Created from HEAD +1f31e97f053caeb5d6b7bffa3faf82941c99efa2 7b7614f8759ac8b5e4b02be65ad8e2667be6dd87 Mitchell Hashimoto 1410886913 -0700 commit: Branch diff --git a/config/module/test-fixtures/basic-git/DOTgit/objects/1f/31e97f053caeb5d6b7bffa3faf82941c99efa2 b/config/module/test-fixtures/basic-git/DOTgit/objects/1f/31e97f053caeb5d6b7bffa3faf82941c99efa2 new file mode 100644 index 0000000000..5793a840b7 Binary files /dev/null and b/config/module/test-fixtures/basic-git/DOTgit/objects/1f/31e97f053caeb5d6b7bffa3faf82941c99efa2 differ diff --git a/config/module/test-fixtures/basic-git/DOTgit/objects/24/3f0fc5c4e586d1a3daa54c981b6f34e9ab1085 b/config/module/test-fixtures/basic-git/DOTgit/objects/24/3f0fc5c4e586d1a3daa54c981b6f34e9ab1085 new file mode 100644 index 0000000000..19819238b8 Binary files /dev/null and b/config/module/test-fixtures/basic-git/DOTgit/objects/24/3f0fc5c4e586d1a3daa54c981b6f34e9ab1085 differ diff --git a/config/module/test-fixtures/basic-git/DOTgit/objects/38/30637158f774a20edcc0bf1c4d07b0bf87c43d b/config/module/test-fixtures/basic-git/DOTgit/objects/38/30637158f774a20edcc0bf1c4d07b0bf87c43d new file mode 100644 index 0000000000..ef8ebf7282 Binary files /dev/null and b/config/module/test-fixtures/basic-git/DOTgit/objects/38/30637158f774a20edcc0bf1c4d07b0bf87c43d differ diff --git a/config/module/test-fixtures/basic-git/DOTgit/objects/40/4618c9d96dfa0a5d365b518e0dfbb5a387c649 b/config/module/test-fixtures/basic-git/DOTgit/objects/40/4618c9d96dfa0a5d365b518e0dfbb5a387c649 new file mode 100644 index 0000000000..434fcab209 Binary files /dev/null and b/config/module/test-fixtures/basic-git/DOTgit/objects/40/4618c9d96dfa0a5d365b518e0dfbb5a387c649 differ diff --git a/config/module/test-fixtures/basic-git/DOTgit/objects/49/7bc37401eb3c9b11865b1768725b64066eccee b/config/module/test-fixtures/basic-git/DOTgit/objects/49/7bc37401eb3c9b11865b1768725b64066eccee new file mode 100644 index 0000000000..ebf34c65df --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/objects/49/7bc37401eb3c9b11865b1768725b64066eccee @@ -0,0 +1,2 @@ +xM +1 @a=E.MZѝkx >x_U-g&xG EByA >/)}kT….ѸSl HjqH %D \ No newline at end of file diff --git a/config/module/test-fixtures/basic-git/DOTgit/objects/7b/7614f8759ac8b5e4b02be65ad8e2667be6dd87 b/config/module/test-fixtures/basic-git/DOTgit/objects/7b/7614f8759ac8b5e4b02be65ad8e2667be6dd87 new file mode 100644 index 0000000000..abe281a740 --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/objects/7b/7614f8759ac8b5e4b02be65ad8e2667be6dd87 @@ -0,0 +1,2 @@ +xK +0aYE6`I7#'.&FbܿEpN?'Zg#r->l&`&Qdр.Y:nKR#aT&"(s23(2Ru7xi?򨰬Sj̥{G`k-S \ No newline at end of file diff --git a/config/module/test-fixtures/basic-git/DOTgit/objects/96/43088174e25a9bd91c27970a580af0085c9f32 b/config/module/test-fixtures/basic-git/DOTgit/objects/96/43088174e25a9bd91c27970a580af0085c9f32 new file mode 100644 index 0000000000..387943288d Binary files /dev/null and b/config/module/test-fixtures/basic-git/DOTgit/objects/96/43088174e25a9bd91c27970a580af0085c9f32 differ diff --git a/config/module/test-fixtures/basic-git/DOTgit/objects/b7/757b6a3696ad036e9aa2f5b4856d09e7f17993 b/config/module/test-fixtures/basic-git/DOTgit/objects/b7/757b6a3696ad036e9aa2f5b4856d09e7f17993 new file mode 100644 index 0000000000..1019256659 Binary files /dev/null and b/config/module/test-fixtures/basic-git/DOTgit/objects/b7/757b6a3696ad036e9aa2f5b4856d09e7f17993 differ diff --git a/config/module/test-fixtures/basic-git/DOTgit/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 b/config/module/test-fixtures/basic-git/DOTgit/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 new file mode 100644 index 0000000000..7112238943 Binary files /dev/null and b/config/module/test-fixtures/basic-git/DOTgit/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 differ diff --git a/config/module/test-fixtures/basic-git/DOTgit/refs/heads/master b/config/module/test-fixtures/basic-git/DOTgit/refs/heads/master new file mode 100644 index 0000000000..91c51fe572 --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/refs/heads/master @@ -0,0 +1 @@ +1f31e97f053caeb5d6b7bffa3faf82941c99efa2 diff --git a/config/module/test-fixtures/basic-git/DOTgit/refs/heads/test-branch b/config/module/test-fixtures/basic-git/DOTgit/refs/heads/test-branch new file mode 100644 index 0000000000..a5f298b839 --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/refs/heads/test-branch @@ -0,0 +1 @@ +7b7614f8759ac8b5e4b02be65ad8e2667be6dd87 diff --git a/config/module/test-fixtures/basic-git/DOTgit/refs/tags/v1.0 b/config/module/test-fixtures/basic-git/DOTgit/refs/tags/v1.0 new file mode 100644 index 0000000000..ada519059e --- /dev/null +++ b/config/module/test-fixtures/basic-git/DOTgit/refs/tags/v1.0 @@ -0,0 +1 @@ +243f0fc5c4e586d1a3daa54c981b6f34e9ab1085 diff --git a/config/module/test-fixtures/basic-git/main.tf b/config/module/test-fixtures/basic-git/main.tf new file mode 100644 index 0000000000..3830637158 --- /dev/null +++ b/config/module/test-fixtures/basic-git/main.tf @@ -0,0 +1,5 @@ +# Hello + +module "foo" { + source = "./foo" +} diff --git a/config/module/test-fixtures/basic-hg/.hg/00changelog.i b/config/module/test-fixtures/basic-hg/.hg/00changelog.i new file mode 100644 index 0000000000..d3a8311050 Binary files /dev/null and b/config/module/test-fixtures/basic-hg/.hg/00changelog.i differ diff --git a/config/module/test-fixtures/basic-hg/.hg/branch b/config/module/test-fixtures/basic-hg/.hg/branch new file mode 100644 index 0000000000..4ad96d5159 --- /dev/null +++ b/config/module/test-fixtures/basic-hg/.hg/branch @@ -0,0 +1 @@ +default diff --git a/config/module/test-fixtures/basic-hg/.hg/cache/branch2-served b/config/module/test-fixtures/basic-hg/.hg/cache/branch2-served new file mode 100644 index 0000000000..f2a9aae94a --- /dev/null +++ b/config/module/test-fixtures/basic-hg/.hg/cache/branch2-served @@ -0,0 +1,3 @@ +c65e998d747ffbb1fe3b1c067a50664bb3fb5da4 1 +dcaed7754d58264cb9a5916215a5442377307bd1 o default +c65e998d747ffbb1fe3b1c067a50664bb3fb5da4 o test-branch diff --git a/config/module/test-fixtures/basic-hg/.hg/cache/tags b/config/module/test-fixtures/basic-hg/.hg/cache/tags new file mode 100644 index 0000000000..b30a3de436 --- /dev/null +++ b/config/module/test-fixtures/basic-hg/.hg/cache/tags @@ -0,0 +1,2 @@ +1 c65e998d747ffbb1fe3b1c067a50664bb3fb5da4 + diff --git a/config/module/test-fixtures/basic-hg/.hg/dirstate b/config/module/test-fixtures/basic-hg/.hg/dirstate new file mode 100644 index 0000000000..53f3a9bc6f Binary files /dev/null and b/config/module/test-fixtures/basic-hg/.hg/dirstate differ diff --git a/config/module/test-fixtures/basic-hg/.hg/last-message.txt b/config/module/test-fixtures/basic-hg/.hg/last-message.txt new file mode 100644 index 0000000000..a24e1a3f2b --- /dev/null +++ b/config/module/test-fixtures/basic-hg/.hg/last-message.txt @@ -0,0 +1,2 @@ +Branch + diff --git a/config/module/test-fixtures/basic-hg/.hg/requires b/config/module/test-fixtures/basic-hg/.hg/requires new file mode 100644 index 0000000000..f634f664bf --- /dev/null +++ b/config/module/test-fixtures/basic-hg/.hg/requires @@ -0,0 +1,4 @@ +dotencode +fncache +revlogv1 +store diff --git a/config/module/test-fixtures/basic-hg/.hg/store/00changelog.i b/config/module/test-fixtures/basic-hg/.hg/store/00changelog.i new file mode 100644 index 0000000000..b3dc2666ac Binary files /dev/null and b/config/module/test-fixtures/basic-hg/.hg/store/00changelog.i differ diff --git a/config/module/test-fixtures/basic-hg/.hg/store/00manifest.i b/config/module/test-fixtures/basic-hg/.hg/store/00manifest.i new file mode 100644 index 0000000000..e35c6bf121 Binary files /dev/null and b/config/module/test-fixtures/basic-hg/.hg/store/00manifest.i differ diff --git a/config/module/test-fixtures/basic-hg/.hg/store/data/main.tf.i b/config/module/test-fixtures/basic-hg/.hg/store/data/main.tf.i new file mode 100644 index 0000000000..f45ddc33f1 Binary files /dev/null and b/config/module/test-fixtures/basic-hg/.hg/store/data/main.tf.i differ diff --git a/config/module/test-fixtures/basic-hg/.hg/store/data/main__branch.tf.i b/config/module/test-fixtures/basic-hg/.hg/store/data/main__branch.tf.i new file mode 100644 index 0000000000..a6bdf46f10 Binary files /dev/null and b/config/module/test-fixtures/basic-hg/.hg/store/data/main__branch.tf.i differ diff --git a/config/module/test-fixtures/basic-hg/.hg/store/fncache b/config/module/test-fixtures/basic-hg/.hg/store/fncache new file mode 100644 index 0000000000..a1babe068d --- /dev/null +++ b/config/module/test-fixtures/basic-hg/.hg/store/fncache @@ -0,0 +1,2 @@ +data/main.tf.i +data/main_branch.tf.i diff --git a/config/module/test-fixtures/basic-hg/.hg/store/phaseroots b/config/module/test-fixtures/basic-hg/.hg/store/phaseroots new file mode 100644 index 0000000000..a08565294c --- /dev/null +++ b/config/module/test-fixtures/basic-hg/.hg/store/phaseroots @@ -0,0 +1 @@ +1 dcaed7754d58264cb9a5916215a5442377307bd1 diff --git a/config/module/test-fixtures/basic-hg/.hg/store/undo b/config/module/test-fixtures/basic-hg/.hg/store/undo new file mode 100644 index 0000000000..cf2be297d7 Binary files /dev/null and b/config/module/test-fixtures/basic-hg/.hg/store/undo differ diff --git a/config/module/test-fixtures/basic-hg/.hg/store/undo.phaseroots b/config/module/test-fixtures/basic-hg/.hg/store/undo.phaseroots new file mode 100644 index 0000000000..a08565294c --- /dev/null +++ b/config/module/test-fixtures/basic-hg/.hg/store/undo.phaseroots @@ -0,0 +1 @@ +1 dcaed7754d58264cb9a5916215a5442377307bd1 diff --git a/config/module/test-fixtures/basic-hg/.hg/undo.bookmarks b/config/module/test-fixtures/basic-hg/.hg/undo.bookmarks new file mode 100644 index 0000000000..e69de29bb2 diff --git a/config/module/test-fixtures/basic-hg/.hg/undo.branch b/config/module/test-fixtures/basic-hg/.hg/undo.branch new file mode 100644 index 0000000000..a81bc2dd2c --- /dev/null +++ b/config/module/test-fixtures/basic-hg/.hg/undo.branch @@ -0,0 +1 @@ +test-branch \ No newline at end of file diff --git a/config/module/test-fixtures/basic-hg/.hg/undo.desc b/config/module/test-fixtures/basic-hg/.hg/undo.desc new file mode 100644 index 0000000000..d678f64de8 --- /dev/null +++ b/config/module/test-fixtures/basic-hg/.hg/undo.desc @@ -0,0 +1,2 @@ +1 +commit diff --git a/config/module/test-fixtures/basic-hg/.hg/undo.dirstate b/config/module/test-fixtures/basic-hg/.hg/undo.dirstate new file mode 100644 index 0000000000..62e4ca2e91 Binary files /dev/null and b/config/module/test-fixtures/basic-hg/.hg/undo.dirstate differ diff --git a/config/module/test-fixtures/basic-hg/main.tf b/config/module/test-fixtures/basic-hg/main.tf new file mode 100644 index 0000000000..3830637158 --- /dev/null +++ b/config/module/test-fixtures/basic-hg/main.tf @@ -0,0 +1,5 @@ +# Hello + +module "foo" { + source = "./foo" +} diff --git a/config/module/test-fixtures/basic/foo/main.tf b/config/module/test-fixtures/basic/foo/main.tf new file mode 100644 index 0000000000..fec56017dc --- /dev/null +++ b/config/module/test-fixtures/basic/foo/main.tf @@ -0,0 +1 @@ +# Hello diff --git a/config/module/test-fixtures/basic/main.tf b/config/module/test-fixtures/basic/main.tf new file mode 100644 index 0000000000..3830637158 --- /dev/null +++ b/config/module/test-fixtures/basic/main.tf @@ -0,0 +1,5 @@ +# Hello + +module "foo" { + source = "./foo" +} diff --git a/config/module/test-fixtures/dup/foo/main.tf b/config/module/test-fixtures/dup/foo/main.tf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/config/module/test-fixtures/dup/main.tf b/config/module/test-fixtures/dup/main.tf new file mode 100644 index 0000000000..98efd6e4ff --- /dev/null +++ b/config/module/test-fixtures/dup/main.tf @@ -0,0 +1,7 @@ +module "foo" { + source = "./foo" +} + +module "foo" { + source = "./foo" +} diff --git a/config/module/test-fixtures/validate-bad-output/child/main.tf b/config/module/test-fixtures/validate-bad-output/child/main.tf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/config/module/test-fixtures/validate-bad-output/main.tf b/config/module/test-fixtures/validate-bad-output/main.tf new file mode 100644 index 0000000000..a19233e12d --- /dev/null +++ b/config/module/test-fixtures/validate-bad-output/main.tf @@ -0,0 +1,7 @@ +module "child" { + source = "./child" +} + +resource "aws_instance" "foo" { + memory = "${module.child.memory}" +} diff --git a/config/module/test-fixtures/validate-bad-var/child/main.tf b/config/module/test-fixtures/validate-bad-var/child/main.tf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/config/module/test-fixtures/validate-bad-var/main.tf b/config/module/test-fixtures/validate-bad-var/main.tf new file mode 100644 index 0000000000..7cc785d178 --- /dev/null +++ b/config/module/test-fixtures/validate-bad-var/main.tf @@ -0,0 +1,5 @@ +module "child" { + source = "./child" + + memory = "foo" +} diff --git a/config/module/test-fixtures/validate-child-bad/child/main.tf b/config/module/test-fixtures/validate-child-bad/child/main.tf new file mode 100644 index 0000000000..93b3654033 --- /dev/null +++ b/config/module/test-fixtures/validate-child-bad/child/main.tf @@ -0,0 +1,3 @@ +# Duplicate resources +resource "aws_instance" "foo" {} +resource "aws_instance" "foo" {} diff --git a/config/module/test-fixtures/validate-child-bad/main.tf b/config/module/test-fixtures/validate-child-bad/main.tf new file mode 100644 index 0000000000..813f7ef8ec --- /dev/null +++ b/config/module/test-fixtures/validate-child-bad/main.tf @@ -0,0 +1,3 @@ +module "foo" { + source = "./child" +} diff --git a/config/module/test-fixtures/validate-child-good/child/main.tf b/config/module/test-fixtures/validate-child-good/child/main.tf new file mode 100644 index 0000000000..2cfd2a80f5 --- /dev/null +++ b/config/module/test-fixtures/validate-child-good/child/main.tf @@ -0,0 +1,3 @@ +variable "memory" {} + +output "result" {} diff --git a/config/module/test-fixtures/validate-child-good/main.tf b/config/module/test-fixtures/validate-child-good/main.tf new file mode 100644 index 0000000000..5f3ad8da5b --- /dev/null +++ b/config/module/test-fixtures/validate-child-good/main.tf @@ -0,0 +1,8 @@ +module "child" { + source = "./child" + memory = "1G" +} + +resource "aws_instance" "foo" { + memory = "${module.child.result}" +} diff --git a/config/module/test-fixtures/validate-root-bad/main.tf b/config/module/test-fixtures/validate-root-bad/main.tf new file mode 100644 index 0000000000..93b3654033 --- /dev/null +++ b/config/module/test-fixtures/validate-root-bad/main.tf @@ -0,0 +1,3 @@ +# Duplicate resources +resource "aws_instance" "foo" {} +resource "aws_instance" "foo" {} diff --git a/config/module/tree.go b/config/module/tree.go new file mode 100644 index 0000000000..558dcce9d5 --- /dev/null +++ b/config/module/tree.go @@ -0,0 +1,320 @@ +package module + +import ( + "bufio" + "bytes" + "fmt" + "strings" + "sync" + + "github.com/hashicorp/terraform/config" +) + +// Tree represents the module import tree of configurations. +// +// This Tree structure can be used to get (download) new modules, load +// all the modules without getting, flatten the tree into something +// Terraform can use, etc. +type Tree struct { + name string + config *config.Config + children map[string]*Tree + lock sync.RWMutex +} + +// GetMode is an enum that describes how modules are loaded. +// +// GetModeLoad says that modules will not be downloaded or updated, they will +// only be loaded from the storage. +// +// GetModeGet says that modules can be initially downloaded if they don't +// exist, but otherwise to just load from the current version in storage. +// +// GetModeUpdate says that modules should be checked for updates and +// downloaded prior to loading. If there are no updates, we load the version +// from disk, otherwise we download first and then load. +type GetMode byte + +const ( + GetModeNone GetMode = iota + GetModeGet + GetModeUpdate +) + +// NewTree returns a new Tree for the given config structure. +func NewTree(name string, c *config.Config) *Tree { + return &Tree{config: c, name: name} +} + +// NewTreeModule is like NewTree except it parses the configuration in +// the directory and gives it a specific name. Use a blank name "" to specify +// the root module. +func NewTreeModule(name, dir string) (*Tree, error) { + c, err := config.LoadDir(dir) + if err != nil { + return nil, err + } + + return NewTree(name, c), nil +} + +// Children returns the children of this tree (the modules that are +// imported by this root). +// +// This will only return a non-nil value after Load is called. +func (t *Tree) Children() map[string]*Tree { + t.lock.RLock() + defer t.lock.RUnlock() + return t.children +} + +// Loaded says whether or not this tree has been loaded or not yet. +func (t *Tree) Loaded() bool { + t.lock.RLock() + defer t.lock.RUnlock() + return t.children != nil +} + +// Modules returns the list of modules that this tree imports. +// +// This is only the imports of _this_ level of the tree. To retrieve the +// full nested imports, you'll have to traverse the tree. +func (t *Tree) Modules() []*Module { + result := make([]*Module, len(t.config.Modules)) + for i, m := range t.config.Modules { + result[i] = &Module{ + Name: m.Name, + Source: m.Source, + } + } + + return result +} + +// Name returns the name of the tree. This will be "" for the root +// tree and then the module name given for any children. +func (t *Tree) Name() string { + if t.name == "" { + return "" + } + + return t.name +} + +// Load loads the configuration of the entire tree. +// +// The parameters are used to tell the tree where to find modules and +// whether it can download/update modules along the way. +// +// Calling this multiple times will reload the tree. +// +// Various semantic-like checks are made along the way of loading since +// module trees inherently require the configuration to be in a reasonably +// sane state: no circular dependencies, proper module sources, etc. A full +// suite of validations can be done by running Validate (after loading). +func (t *Tree) Load(s Storage, mode GetMode) error { + t.lock.Lock() + defer t.lock.Unlock() + + // Reset the children if we have any + t.children = nil + + modules := t.Modules() + children := make(map[string]*Tree) + + // Go through all the modules and get the directory for them. + update := mode == GetModeUpdate + for _, m := range modules { + if _, ok := children[m.Name]; ok { + return fmt.Errorf( + "module %s: duplicated. module names must be unique", m.Name) + } + + source, err := Detect(m.Source, t.config.Dir) + if err != nil { + return fmt.Errorf("module %s: %s", m.Name, err) + } + + if mode > GetModeNone { + // Get the module since we specified we should + if err := s.Get(source, update); err != nil { + return err + } + } + + // Get the directory where this module is so we can load it + dir, ok, err := s.Dir(source) + if err != nil { + return err + } + if !ok { + return fmt.Errorf( + "module %s: not found, may need to be downloaded", m.Name) + } + + // Load the configuration + children[m.Name], err = NewTreeModule(m.Name, dir) + if err != nil { + return fmt.Errorf( + "module %s: %s", m.Name, err) + } + } + + // Go through all the children and load them. + for _, c := range children { + if err := c.Load(s, mode); err != nil { + return err + } + } + + // Set our tree up + t.children = children + + return nil +} + +// String gives a nice output to describe the tree. +func (t *Tree) String() string { + var result bytes.Buffer + result.WriteString(t.Name() + "\n") + + cs := t.Children() + if cs == nil { + result.WriteString(" not loaded") + } else { + // Go through each child and get its string value, then indent it + // by two. + for _, c := range cs { + r := strings.NewReader(c.String()) + scanner := bufio.NewScanner(r) + for scanner.Scan() { + result.WriteString(" ") + result.WriteString(scanner.Text()) + result.WriteString("\n") + } + } + } + + return result.String() +} + +// Validate does semantic checks on the entire tree of configurations. +// +// This will call the respective config.Config.Validate() functions as well +// as verifying things such as parameters/outputs between the various modules. +// +// Load must be called prior to calling Validate or an error will be returned. +func (t *Tree) Validate() error { + if !t.Loaded() { + return fmt.Errorf("tree must be loaded before calling Validate") + } + + // If something goes wrong, here is our error template + newErr := &TreeError{Name: []string{t.Name()}} + + // Validate our configuration first. + if err := t.config.Validate(); err != nil { + newErr.Err = err + return newErr + } + + // Get the child trees + children := t.Children() + + // Validate all our children + for _, c := range children { + err := c.Validate() + if err == nil { + continue + } + + verr, ok := err.(*TreeError) + if !ok { + // Unknown error, just return... + return err + } + + // Append ourselves to the error and then return + verr.Name = append(verr.Name, t.Name()) + return verr + } + + // Go over all the modules and verify that any parameters are valid + // variables into the module in question. + for _, m := range t.config.Modules { + tree, ok := children[m.Name] + if !ok { + // This should never happen because Load watches us + panic("module not found in children: " + m.Name) + } + + // Build the variables that the module defines + varMap := make(map[string]struct{}) + for _, v := range tree.config.Variables { + varMap[v.Name] = struct{}{} + } + + // Compare to the keys in our raw config for the module + for k, _ := range m.RawConfig.Raw { + if _, ok := varMap[k]; !ok { + newErr.Err = fmt.Errorf( + "module %s: %s is not a valid parameter", + m.Name, k) + return newErr + } + } + } + + // Go over all the variables used and make sure that any module + // variables represent outputs properly. + for source, vs := range t.config.InterpolatedVariables() { + for _, v := range vs { + mv, ok := v.(*config.ModuleVariable) + if !ok { + continue + } + + tree, ok := children[mv.Name] + if !ok { + // This should never happen because Load watches us + panic("module not found in children: " + mv.Name) + } + + found := false + for _, o := range tree.config.Outputs { + if o.Name == mv.Field { + found = true + break + } + } + if !found { + newErr.Err = fmt.Errorf( + "%s: %s is not a valid output for module %s", + source, mv.Field, mv.Name) + return newErr + } + } + } + + return nil +} + +// TreeError is an error returned by Tree.Validate if an error occurs +// with validation. +type TreeError struct { + Name []string + Err error +} + +func (e *TreeError) Error() string { + // Build up the name + var buf bytes.Buffer + for _, n := range e.Name { + buf.WriteString(n) + buf.WriteString(".") + } + buf.Truncate(buf.Len() - 1) + + // Format the value + return fmt.Sprintf("module %s: %s", buf.String(), e.Err) +} diff --git a/config/module/tree_test.go b/config/module/tree_test.go new file mode 100644 index 0000000000..95c481ddee --- /dev/null +++ b/config/module/tree_test.go @@ -0,0 +1,155 @@ +package module + +import ( + "reflect" + "strings" + "testing" +) + +func TestTreeLoad(t *testing.T) { + storage := testStorage(t) + tree := NewTree("", testConfig(t, "basic")) + + if tree.Loaded() { + t.Fatal("should not be loaded") + } + + // This should error because we haven't gotten things yet + if err := tree.Load(storage, GetModeNone); err == nil { + t.Fatal("should error") + } + + if tree.Loaded() { + t.Fatal("should not be loaded") + } + + // This should get things + if err := tree.Load(storage, GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if !tree.Loaded() { + t.Fatal("should be loaded") + } + + // This should no longer error + if err := tree.Load(storage, GetModeNone); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(tree.String()) + expected := strings.TrimSpace(treeLoadStr) + if actual != expected { + t.Fatalf("bad: \n\n%s", actual) + } +} + +func TestTreeLoad_duplicate(t *testing.T) { + storage := testStorage(t) + tree := NewTree("", testConfig(t, "dup")) + + if tree.Loaded() { + t.Fatal("should not be loaded") + } + + // This should get things + if err := tree.Load(storage, GetModeGet); err == nil { + t.Fatalf("should error") + } +} + +func TestTreeModules(t *testing.T) { + tree := NewTree("", testConfig(t, "basic")) + actual := tree.Modules() + + expected := []*Module{ + &Module{Name: "foo", Source: "./foo"}, + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} + +func TestTreeName(t *testing.T) { + tree := NewTree("", testConfig(t, "basic")) + actual := tree.Name() + + if actual != "" { + t.Fatalf("bad: %#v", actual) + } +} + +func TestTreeValidate_badChild(t *testing.T) { + tree := NewTree("", testConfig(t, "validate-child-bad")) + + if err := tree.Load(testStorage(t), GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if err := tree.Validate(); err == nil { + t.Fatal("should error") + } +} + +func TestTreeValidate_badChildOutput(t *testing.T) { + tree := NewTree("", testConfig(t, "validate-bad-output")) + + if err := tree.Load(testStorage(t), GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if err := tree.Validate(); err == nil { + t.Fatal("should error") + } +} + +func TestTreeValidate_badChildVar(t *testing.T) { + tree := NewTree("", testConfig(t, "validate-bad-var")) + + if err := tree.Load(testStorage(t), GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if err := tree.Validate(); err == nil { + t.Fatal("should error") + } +} + +func TestTreeValidate_badRoot(t *testing.T) { + tree := NewTree("", testConfig(t, "validate-root-bad")) + + if err := tree.Load(testStorage(t), GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if err := tree.Validate(); err == nil { + t.Fatal("should error") + } +} + +func TestTreeValidate_good(t *testing.T) { + tree := NewTree("", testConfig(t, "validate-child-good")) + + if err := tree.Load(testStorage(t), GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if err := tree.Validate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestTreeValidate_notLoaded(t *testing.T) { + tree := NewTree("", testConfig(t, "basic")) + + if err := tree.Validate(); err == nil { + t.Fatal("should error") + } +} + + +const treeLoadStr = ` + + foo +` diff --git a/config/test-fixtures/empty.tf b/config/test-fixtures/empty.tf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/config/test-fixtures/modules.tf b/config/test-fixtures/modules.tf new file mode 100644 index 0000000000..dc1fb6a793 --- /dev/null +++ b/config/test-fixtures/modules.tf @@ -0,0 +1,4 @@ +module "bar" { + memory = "1G" + source = "baz" +} diff --git a/config/test-fixtures/validate-dup-module/main.tf b/config/test-fixtures/validate-dup-module/main.tf new file mode 100644 index 0000000000..cf7fd3d9c8 --- /dev/null +++ b/config/test-fixtures/validate-dup-module/main.tf @@ -0,0 +1,7 @@ +module "aws_instance" "web" { + source = "foo" +} + +module "aws_instance" "web" { + source = "bar" +} diff --git a/config/test-fixtures/validate-var-module-invalid/main.tf b/config/test-fixtures/validate-var-module-invalid/main.tf new file mode 100644 index 0000000000..73897c0086 --- /dev/null +++ b/config/test-fixtures/validate-var-module-invalid/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "foo" { + foo = "${module.foo.bar}" +} diff --git a/config/test-fixtures/validate-var-module/main.tf b/config/test-fixtures/validate-var-module/main.tf new file mode 100644 index 0000000000..402c235a1f --- /dev/null +++ b/config/test-fixtures/validate-var-module/main.tf @@ -0,0 +1,5 @@ +module "foo" {} + +resource "aws_instance" "foo" { + foo = "${module.foo.bar}" +}