From ec3f72703c82f50787ef86498d856a5798900ad8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 22 May 2014 16:56:28 -0700 Subject: [PATCH] Initial work on config --- .gitignore | 3 + Makefile | 38 ++++++++++ config/config.go | 19 +++++ config/config_test.go | 4 + config/loader.go | 134 ++++++++++++++++++++++++++++++++++ config/loader_test.go | 75 +++++++++++++++++++ config/test-fixtures/basic.tf | 15 ++++ 7 files changed, 288 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 config/config.go create mode 100644 config/config_test.go create mode 100644 config/loader.go create mode 100644 config/loader_test.go create mode 100644 config/test-fixtures/basic.tf diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..04d705546 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.dll + +example.tf diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..43517ad52 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +CGO_CFLAGS:=-I$(CURDIR)/vendor/libucl/include +CGO_LDFLAGS:=-L$(CURDIR)/vendor/libucl +LIBUCL_NAME=libucl.a +TEST?=./... + +# If we're on Windows, we need to change some variables so things compile +# properly. +ifeq ($(OS), Windows_NT) + LIBUCL_NAME=libucl.dll +endif + +export CGO_CFLAGS CGO_LDFLAGS PATH + +libucl: vendor/libucl/$(LIBUCL_NAME) + +test: libucl + go test $(TEST) + +vendor/libucl/libucl.a: vendor/libucl + cd vendor/libucl && \ + cmake cmake/ && \ + make + +vendor/libucl/libucl.dll: vendor/libucl + cd vendor/libucl && \ + $(MAKE) -f Makefile.w32 && \ + cp .obj/libucl.dll . && \ + cp libucl.dll $(CURDIR) + +vendor/libucl: + rm -rf vendor/libucl + mkdir -p vendor/libucl + git clone https://github.com/vstakhov/libucl.git vendor/libucl + +clean: + rm -rf vendor + +.PHONY: clean libucl test diff --git a/config/config.go b/config/config.go new file mode 100644 index 000000000..4932b3acb --- /dev/null +++ b/config/config.go @@ -0,0 +1,19 @@ +package config + +// Config is the configuration that comes from loading a collection +// of Terraform templates. +type Config struct { + Variables map[string]Variable + Resources []Resource +} + +type Resource struct { + Name string + Type string + Config map[string]interface{} +} + +type Variable struct { + Default string + Description string +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 000000000..756175cc2 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,4 @@ +package config + +// This is the directory where our test fixtures are. +const fixtureDir = "./test-fixtures" diff --git a/config/loader.go b/config/loader.go new file mode 100644 index 000000000..ea11127b6 --- /dev/null +++ b/config/loader.go @@ -0,0 +1,134 @@ +package config + +import ( + "fmt" + + "github.com/mitchellh/go-libucl" +) + +// Put the parse flags we use for libucl in a constant so we can get +// equally behaving parsing everywhere. +const libuclParseFlags = libucl.ParserKeyLowercase + +// Load loads the Terraform configuration from a given file. +func Load(path string) (*Config, error) { + var rawConfig struct { + Variable map[string]Variable + Object *libucl.Object `libucl:",object"` + } + + // Parse the libucl file into the raw format + if err := parseFile(path, &rawConfig); err != nil { + return nil, err + } + + // Make sure we close the raw object + defer rawConfig.Object.Close() + + // Start building up the actual configuration. We first + // copy the fields that can be directly assigned. + config := new(Config) + config.Variables = rawConfig.Variable + + // Build the resources + resources := rawConfig.Object.Get("resource") + if resources != nil { + defer resources.Close() + + var err error + config.Resources, err = loadResourcesLibucl(resources) + if err != nil { + return nil, err + } + } + + return config, nil +} + +func loadResourcesLibucl(o *libucl.Object) ([]Resource, error) { + var allTypes []*libucl.Object + + // Libucl object iteration is really nasty. Below is likely to make + // no sense to anyone approaching this code. Luckily, it is very heavily + // tested. If working on a bug fix or feature, we recommend writing a + // test first then doing whatever you want to the code below. If you + // break it, the tests will catch it. Likewise, if you change this, + // MAKE SURE you write a test for your change, because its fairly impossible + // to reason about this mess. + // + // Functionally, what the code does below is get the libucl.Objects + // for all the TYPES, such as "aws_security_group". + iter := o.Iterate(false) + for o1 := iter.Next(); o1 != nil; o1 = iter.Next() { + // Iterate the inner to get the list of types + iter2 := o1.Iterate(true) + for o2 := iter2.Next(); o2 != nil; o2 = iter2.Next() { + // Iterate all of this type to get _all_ the types + iter3 := o2.Iterate(false) + for o3 := iter3.Next(); o3 != nil; o3 = iter3.Next() { + allTypes = append(allTypes, o3) + } + + o2.Close() + iter3.Close() + } + + o1.Close() + iter2.Close() + } + iter.Close() + + // Where all the results will go + var result []Resource + + // Now go over all the types and their children in order to get + // all of the actual resources. + for _, t := range allTypes { + // Release the resources for this raw type since we don't need it. + // Note that this makes it unsafe now to use allTypes again. + defer t.Close() + + iter := t.Iterate(true) + defer iter.Close() + for r := iter.Next(); r != nil; r = iter.Next() { + defer r.Close() + + var config map[string]interface{} + if err := r.Decode(&config); err != nil { + return nil, fmt.Errorf( + "Error reading config for %s[%s]: %s", + t.Key(), + r.Key(), + err) + } + + result = append(result, Resource{ + Name: r.Key(), + Type: t.Key(), + Config: config, + }) + } + } + + return result, nil +} + +// Helper for parsing a single libucl-formatted file into +// the given structure. +func parseFile(path string, result interface{}) error { + parser := libucl.NewParser(libuclParseFlags) + defer parser.Close() + + if err := parser.AddFile(path); err != nil { + return err + } + + root := parser.Object() + defer root.Close() + + if err := root.Decode(result); err != nil { + return err + } + + return nil +} diff --git a/config/loader_test.go b/config/loader_test.go new file mode 100644 index 000000000..332b74f5c --- /dev/null +++ b/config/loader_test.go @@ -0,0 +1,75 @@ +package config + +import ( + "fmt" + "path/filepath" + "strings" + "testing" +) + +func TestLoadBasic(t *testing.T) { + c, err := Load(filepath.Join(fixtureDir, "basic.tf")) + if err != nil { + t.Fatalf("err: %s", err) + } + + if c == nil { + t.Fatal("config should not be nil") + } + + actual := variablesStr(c.Variables) + if actual != strings.TrimSpace(basicVariablesStr) { + t.Fatalf("bad:\n%s", actual) + } + + actual = resourcesStr(c.Resources) + if actual != strings.TrimSpace(basicResourcesStr) { + t.Fatalf("bad:\n%s", actual) + } +} + +// This helper turns a resources field into a deterministic +// string value for comparison in tests. +func resourcesStr(rs []Resource) string { + result := "" + for _, r := range rs { + result += fmt.Sprintf( + "%s[%s]\n", + r.Type, + r.Name) + + for k, _ := range r.Config { + result += fmt.Sprintf(" %s\n", k) + } + } + + return strings.TrimSpace(result) +} + +// This helper turns a variables field into a deterministic +// string value for comparison in tests. +func variablesStr(vs map[string]Variable) string { + result := "" + for k, v := range vs { + result += fmt.Sprintf( + "%s\n %s\n %s\n", + k, + v.Default, + v.Description) + } + + return strings.TrimSpace(result) +} + +const basicResourcesStr = ` +aws_security_group[firewall] +aws_instance[web] + ami + security_groups +` + +const basicVariablesStr = ` +foo + bar + bar +` diff --git a/config/test-fixtures/basic.tf b/config/test-fixtures/basic.tf new file mode 100644 index 000000000..846ee41f5 --- /dev/null +++ b/config/test-fixtures/basic.tf @@ -0,0 +1,15 @@ +variable "foo" { + default = "bar"; + description = "bar"; +} + +resource "aws_security_group" "firewall" { +} + +resource aws_instance "web" { + ami = "ami-123456" + security_groups = [ + "foo", + "${aws_security_group.firewall.foo}" + ] +}