Initial work on config
This commit is contained in:
parent
649cf336e8
commit
ec3f72703c
|
@ -0,0 +1,3 @@
|
|||
*.dll
|
||||
|
||||
example.tf
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package config
|
||||
|
||||
// This is the directory where our test fixtures are.
|
||||
const fixtureDir = "./test-fixtures"
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
`
|
|
@ -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}"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue