Merge pull request #14 from hashicorp/f-outputs

Outputs
This commit is contained in:
Mitchell Hashimoto 2014-07-05 10:55:16 -07:00
commit 41eceedc4c
13 changed files with 479 additions and 39 deletions

View File

@ -15,6 +15,7 @@ type Config struct {
ProviderConfigs map[string]*ProviderConfig ProviderConfigs map[string]*ProviderConfig
Resources []*Resource Resources []*Resource
Variables map[string]*Variable Variables map[string]*Variable
Outputs map[string]*Output
} }
// ProviderConfig is the configuration for a resource provider. // ProviderConfig is the configuration for a resource provider.
@ -42,6 +43,13 @@ type Variable struct {
defaultSet bool defaultSet bool
} }
// Output is an output defined within the configuration. An output is
// resulting data that is highlighted by Terraform when finished.
type Output struct {
Name string
RawConfig *RawConfig
}
// An InterpolatedVariable is a variable that is embedded within a string // An InterpolatedVariable is a variable that is embedded within a string
// in the configuration, such as "hello ${world}" (world in this case is // in the configuration, such as "hello ${world}" (world in this case is
// an interpolated variable). // an interpolated variable).
@ -55,9 +63,10 @@ type InterpolatedVariable interface {
// A ResourceVariable is a variable that is referencing the field // A ResourceVariable is a variable that is referencing the field
// of a resource, such as "${aws_instance.foo.ami}" // of a resource, such as "${aws_instance.foo.ami}"
type ResourceVariable struct { type ResourceVariable struct {
Type string Type string // Resource type, i.e. "aws_instance"
Name string Name string // Resource name
Field string Field string // Resource field
Multi bool // True if multi-variable: aws_instance.foo.*.id
key string key string
} }
@ -98,17 +107,19 @@ func (c *Config) Validate() error {
// Check for references to user variables that do not actually // Check for references to user variables that do not actually
// exist and record those errors. // exist and record those errors.
for source, v := range vars { for source, vs := range vars {
uv, ok := v.(*UserVariable) for _, v := range vs {
if !ok { uv, ok := v.(*UserVariable)
continue if !ok {
} continue
}
if _, ok := c.Variables[uv.Name]; !ok { if _, ok := c.Variables[uv.Name]; !ok {
errs = append(errs, fmt.Errorf( errs = append(errs, fmt.Errorf(
"%s: unknown variable referenced: %s", "%s: unknown variable referenced: %s",
source, source,
uv.Name)) uv.Name))
}
} }
} }
@ -117,19 +128,36 @@ func (c *Config) Validate() error {
for _, r := range c.Resources { for _, r := range c.Resources {
resources[r.Id()] = struct{}{} resources[r.Id()] = struct{}{}
} }
for source, v := range vars { for source, vs := range vars {
rv, ok := v.(*ResourceVariable) for _, v := range vs {
if !ok { rv, ok := v.(*ResourceVariable)
continue if !ok {
} continue
}
id := fmt.Sprintf("%s.%s", rv.Type, rv.Name) id := fmt.Sprintf("%s.%s", rv.Type, rv.Name)
if _, ok := resources[id]; !ok { if _, ok := resources[id]; !ok {
errs = append(errs, fmt.Errorf(
"%s: unknown resource '%s' referenced in variable %s",
source,
id,
rv.FullKey()))
}
}
}
// Check that all outputs are valid
for _, o := range c.Outputs {
invalid := false
for k, _ := range o.RawConfig.Raw {
if k != "value" {
invalid = true
break
}
}
if invalid {
errs = append(errs, fmt.Errorf( errs = append(errs, fmt.Errorf(
"%s: unknown resource '%s' referenced in variable %s", "%s: output should only have 'value' field", o.Name))
source,
id,
rv.FullKey()))
} }
} }
@ -143,19 +171,26 @@ func (c *Config) Validate() error {
// allVariables is a helper that returns a mapping of all the interpolated // allVariables is a helper that returns a mapping of all the interpolated
// variables within the configuration. This is used to verify references // variables within the configuration. This is used to verify references
// are valid in the Validate step. // are valid in the Validate step.
func (c *Config) allVariables() map[string]InterpolatedVariable { func (c *Config) allVariables() map[string][]InterpolatedVariable {
result := make(map[string]InterpolatedVariable) result := make(map[string][]InterpolatedVariable)
for n, pc := range c.ProviderConfigs { for n, pc := range c.ProviderConfigs {
source := fmt.Sprintf("provider config '%s'", n) source := fmt.Sprintf("provider config '%s'", n)
for _, v := range pc.RawConfig.Variables { for _, v := range pc.RawConfig.Variables {
result[source] = v result[source] = append(result[source], v)
} }
} }
for _, rc := range c.Resources { for _, rc := range c.Resources {
source := fmt.Sprintf("resource '%s'", rc.Id()) source := fmt.Sprintf("resource '%s'", rc.Id())
for _, v := range rc.RawConfig.Variables { for _, v := range rc.RawConfig.Variables {
result[source] = v result[source] = append(result[source], v)
}
}
for _, o := range c.Outputs {
source := fmt.Sprintf("output '%s'", o.Name)
for _, v := range o.RawConfig.Variables {
result[source] = append(result[source], v)
} }
} }
@ -169,10 +204,19 @@ func (v *Variable) Required() bool {
func NewResourceVariable(key string) (*ResourceVariable, error) { func NewResourceVariable(key string) (*ResourceVariable, error) {
parts := strings.SplitN(key, ".", 3) parts := strings.SplitN(key, ".", 3)
field := parts[2]
multi := false
if idx := strings.Index(field, "."); idx != -1 && field[:idx] == "*" {
multi = true
field = field[idx+1:]
}
return &ResourceVariable{ return &ResourceVariable{
Type: parts[0], Type: parts[0],
Name: parts[1], Name: parts[1],
Field: parts[2], Field: field,
Multi: multi,
key: key, key: key,
}, nil }, nil
} }

View File

@ -15,6 +15,13 @@ func TestConfigValidate(t *testing.T) {
} }
} }
func TestConfigValidate_outputBadField(t *testing.T) {
c := testConfig(t, "validate-output-bad-field")
if err := c.Validate(); err == nil {
t.Fatal("should not be valid")
}
}
func TestConfigValidate_unknownResourceVar(t *testing.T) { func TestConfigValidate_unknownResourceVar(t *testing.T) {
c := testConfig(t, "validate-unknown-resource-var") c := testConfig(t, "validate-unknown-resource-var")
if err := c.Validate(); err == nil { if err := c.Validate(); err == nil {
@ -22,6 +29,13 @@ func TestConfigValidate_unknownResourceVar(t *testing.T) {
} }
} }
func TestConfigValidate_unknownResourceVar_output(t *testing.T) {
c := testConfig(t, "validate-unknown-resource-var-output")
if err := c.Validate(); err == nil {
t.Fatal("should not be valid")
}
}
func TestConfigValidate_unknownVar(t *testing.T) { func TestConfigValidate_unknownVar(t *testing.T) {
c := testConfig(t, "validate-unknownvar") c := testConfig(t, "validate-unknownvar")
if err := c.Validate(); err == nil { if err := c.Validate(); err == nil {
@ -44,12 +58,35 @@ func TestNewResourceVariable(t *testing.T) {
if v.Field != "baz" { if v.Field != "baz" {
t.Fatalf("bad: %#v", v) t.Fatalf("bad: %#v", v)
} }
if v.Multi {
t.Fatal("should not be multi")
}
if v.FullKey() != "foo.bar.baz" { if v.FullKey() != "foo.bar.baz" {
t.Fatalf("bad: %#v", v) t.Fatalf("bad: %#v", v)
} }
} }
func TestResourceVariable_Multi(t *testing.T) {
v, err := NewResourceVariable("foo.bar.*.baz")
if err != nil {
t.Fatalf("err: %s", err)
}
if v.Type != "foo" {
t.Fatalf("bad: %#v", v)
}
if v.Name != "bar" {
t.Fatalf("bad: %#v", v)
}
if v.Field != "baz" {
t.Fatalf("bad: %#v", v)
}
if !v.Multi {
t.Fatal("should be multi")
}
}
func TestNewUserVariable(t *testing.T) { func TestNewUserVariable(t *testing.T) {
v, err := NewUserVariable("var.bar") v, err := NewUserVariable("var.bar")
if err != nil { if err != nil {

View File

@ -78,6 +78,16 @@ func (t *libuclConfigurable) Config() (*Config, error) {
} }
} }
// Build the outputs
if outputs := t.Object.Get("output"); outputs != nil {
var err error
config.Outputs, err = loadOutputsLibucl(outputs)
outputs.Close()
if err != nil {
return nil, err
}
}
return config, nil return config, nil
} }
@ -145,6 +155,52 @@ func loadFileLibucl(root string) (configurable, []string, error) {
return result, importPaths, nil return result, importPaths, nil
} }
// LoadOutputsLibucl recurses into the given libucl object and turns
// it into a mapping of outputs.
func loadOutputsLibucl(o *libucl.Object) (map[string]*Output, error) {
objects := make(map[string]*libucl.Object)
// Iterate over all the "output" blocks and get the keys along with
// their raw configuration objects. We'll parse those later.
iter := o.Iterate(false)
for o1 := iter.Next(); o1 != nil; o1 = iter.Next() {
iter2 := o1.Iterate(true)
for o2 := iter2.Next(); o2 != nil; o2 = iter2.Next() {
objects[o2.Key()] = o2
defer o2.Close()
}
o1.Close()
iter2.Close()
}
iter.Close()
// Go through each object and turn it into an actual result.
result := make(map[string]*Output)
for n, o := range objects {
var config map[string]interface{}
if err := o.Decode(&config); err != nil {
return nil, err
}
rawConfig, err := NewRawConfig(config)
if err != nil {
return nil, fmt.Errorf(
"Error reading config for output %s: %s",
n,
err)
}
result[n] = &Output{
Name: n,
RawConfig: rawConfig,
}
}
return result, nil
}
// LoadProvidersLibucl recurses into the given libucl object and turns // LoadProvidersLibucl recurses into the given libucl object and turns
// it into a mapping of provider configs. // it into a mapping of provider configs.
func loadProvidersLibucl(o *libucl.Object) (map[string]*ProviderConfig, error) { func loadProvidersLibucl(o *libucl.Object) (map[string]*ProviderConfig, error) {

View File

@ -39,6 +39,11 @@ func TestLoadBasic(t *testing.T) {
if actual != strings.TrimSpace(basicResourcesStr) { if actual != strings.TrimSpace(basicResourcesStr) {
t.Fatalf("bad:\n%s", actual) t.Fatalf("bad:\n%s", actual)
} }
actual = outputsStr(c.Outputs)
if actual != strings.TrimSpace(basicOutputsStr) {
t.Fatalf("bad:\n%s", actual)
}
} }
func TestLoadBasic_import(t *testing.T) { func TestLoadBasic_import(t *testing.T) {
@ -92,6 +97,40 @@ func TestLoad_variables(t *testing.T) {
} }
} }
func outputsStr(os map[string]*Output) string {
ns := make([]string, 0, len(os))
for n, _ := range os {
ns = append(ns, n)
}
sort.Strings(ns)
result := ""
for _, n := range ns {
o := os[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 // This helper turns a provider configs field into a deterministic
// string value for comparison in tests. // string value for comparison in tests.
func providerConfigsStr(pcs map[string]*ProviderConfig) string { func providerConfigsStr(pcs map[string]*ProviderConfig) string {
@ -219,6 +258,12 @@ func variablesStr(vs map[string]*Variable) string {
return strings.TrimSpace(result) return strings.TrimSpace(result)
} }
const basicOutputsStr = `
web_ip
vars
resource: aws_instance.web.private_ip
`
const basicProvidersStr = ` const basicProvidersStr = `
aws aws
access_key access_key

View File

@ -32,3 +32,7 @@ resource aws_instance "web" {
resource "aws_instance" "db" { resource "aws_instance" "db" {
security_groups = "${aws_security_group.firewall.*.id}" security_groups = "${aws_security_group.firewall.*.id}"
} }
output "web_ip" {
value = "${aws_instance.web.private_ip}"
}

View File

@ -0,0 +1,7 @@
resource "aws_instance" "web" {
}
output "ip" {
value = "foo"
another = "nope"
}

View File

@ -0,0 +1,6 @@
resource "aws_instance" "web" {
}
output "ip" {
value = "${aws_instance.loadbalancer.foo}"
}

View File

@ -105,6 +105,18 @@ func (c *Context) Apply() (*State, error) {
// Update our state, even if we have an error, for partial updates // Update our state, even if we have an error, for partial updates
c.state = s c.state = s
// If we have no errors, then calculate the outputs if we have any
if err == nil && len(c.config.Outputs) > 0 {
s.Outputs = make(map[string]string)
for _, o := range c.config.Outputs {
if err = c.computeVars(o.RawConfig); err != nil {
break
}
s.Outputs[o.Name] = o.RawConfig.Config()["value"].(string)
}
}
return s, err return s, err
} }
@ -232,6 +244,110 @@ func (c *Context) Validate() ([]string, []error) {
return warns, errs return warns, errs
} }
// computeVars takes the State and given RawConfig and processes all
// the variables. This dynamically discovers the attributes instead of
// using a static map[string]string that the genericWalkFn uses.
func (c *Context) computeVars(raw *config.RawConfig) error {
// If there are on variables, then we're done
if len(raw.Variables) == 0 {
return nil
}
// Go through each variable and find it
vs := make(map[string]string)
for n, rawV := range raw.Variables {
switch v := rawV.(type) {
case *config.ResourceVariable:
var attr string
var err error
if v.Multi {
attr, err = c.computeResourceMultiVariable(v)
} else {
attr, err = c.computeResourceVariable(v)
}
if err != nil {
return err
}
vs[n] = attr
case *config.UserVariable:
vs[n] = c.variables[v.Name]
}
}
// Interpolate the variables
return raw.Interpolate(vs)
}
func (c *Context) computeResourceVariable(
v *config.ResourceVariable) (string, error) {
r, ok := c.state.Resources[v.ResourceId()]
if !ok {
return "", fmt.Errorf(
"Resource '%s' not found for variable '%s'",
v.ResourceId(),
v.FullKey())
}
attr, ok := r.Attributes[v.Field]
if !ok {
return "", fmt.Errorf(
"Resource '%s' does not have attribute '%s' "+
"for variable '%s'",
v.ResourceId(),
v.Field,
v.FullKey())
}
return attr, nil
}
func (c *Context) computeResourceMultiVariable(
v *config.ResourceVariable) (string, error) {
// Get the resource from the configuration so we can know how
// many of the resource there is.
var cr *config.Resource
for _, r := range c.config.Resources {
if r.Id() == v.ResourceId() {
cr = r
break
}
}
if cr == nil {
return "", fmt.Errorf(
"Resource '%s' not found for variable '%s'",
v.ResourceId(),
v.FullKey())
}
var values []string
for i := 0; i < cr.Count; i++ {
id := fmt.Sprintf("%s.%d", v.ResourceId(), i)
r, ok := c.state.Resources[id]
if !ok {
continue
}
attr, ok := r.Attributes[v.Field]
if !ok {
continue
}
values = append(values, attr)
}
if len(values) == 0 {
return "", fmt.Errorf(
"Resource '%s' does not have attribute '%s' "+
"for variable '%s'",
v.ResourceId(),
v.Field,
v.FullKey())
}
return strings.Join(values, ","), nil
}
func (c *Context) graph() (*depgraph.Graph, error) { func (c *Context) graph() (*depgraph.Graph, error) {
return Graph(&GraphOpts{ return Graph(&GraphOpts{
Config: c.config, Config: c.config,
@ -647,17 +763,9 @@ func computeAggregateVars(
if !ok { if !ok {
continue continue
} }
if !rv.Multi {
idx := strings.Index(rv.Field, ".")
if idx == -1 {
// It isn't an aggregated var
continue continue
} }
if rv.Field[:idx] != "*" {
// It isn't an aggregated var
continue
}
field := rv.Field[idx+1:]
// Get the meta node so that we can determine the count // Get the meta node so that we can determine the count
key := fmt.Sprintf("%s.%s", rv.Type, rv.Name) key := fmt.Sprintf("%s.%s", rv.Type, rv.Name)
@ -677,7 +785,7 @@ func computeAggregateVars(
rv.Type, rv.Type,
rv.Name, rv.Name,
i, i,
field) rv.Field)
if v, ok := vs[key]; ok { if v, ok := vs[key]; ok {
values = append(values, v) values = append(values, v)
} }

View File

@ -508,6 +508,62 @@ func TestContextApply_hook(t *testing.T) {
} }
} }
func TestContextApply_output(t *testing.T) {
c := testConfig(t, "apply-output")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
ctx := testContext(t, &ContextOpts{
Config: c,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
})
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyOutputStr)
if actual != expected {
t.Fatalf("bad: \n%s", actual)
}
}
func TestContextApply_outputMulti(t *testing.T) {
c := testConfig(t, "apply-output-multi")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
ctx := testContext(t, &ContextOpts{
Config: c,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
})
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyOutputMultiStr)
if actual != expected {
t.Fatalf("bad: \n%s", actual)
}
}
func TestContextApply_unknownAttribute(t *testing.T) { func TestContextApply_unknownAttribute(t *testing.T) {
c := testConfig(t, "apply-unknown") c := testConfig(t, "apply-unknown")
p := testProvider("aws") p := testProvider("aws")

View File

@ -16,6 +16,7 @@ import (
// can use to keep track of what real world resources it is actually // can use to keep track of what real world resources it is actually
// managing. // managing.
type State struct { type State struct {
Outputs map[string]string
Resources map[string]*ResourceState Resources map[string]*ResourceState
once sync.Once once sync.Once
@ -96,6 +97,21 @@ func (s *State) String() string {
} }
} }
if len(s.Outputs) > 0 {
buf.WriteString("\nOutputs:\n\n")
ks := make([]string, 0, len(s.Outputs))
for k, _ := range s.Outputs {
ks = append(ks, k)
}
sort.Strings(ks)
for _, k := range ks {
v := s.Outputs[k]
buf.WriteString(fmt.Sprintf("%s = %s\n", k, v))
}
}
return buf.String() return buf.String()
} }

View File

@ -106,6 +106,44 @@ aws_instance.foo:
num = 2 num = 2
` `
const testTerraformApplyOutputStr = `
aws_instance.bar:
ID = foo
foo = bar
type = aws_instance
aws_instance.foo:
ID = foo
num = 2
type = aws_instance
Outputs:
foo_num = 2
`
const testTerraformApplyOutputMultiStr = `
aws_instance.bar.0:
ID = foo
foo = bar
type = aws_instance
aws_instance.bar.1:
ID = foo
foo = bar
type = aws_instance
aws_instance.bar.2:
ID = foo
foo = bar
type = aws_instance
aws_instance.foo:
ID = foo
num = 2
type = aws_instance
Outputs:
foo_num = bar,bar,bar
`
const testTerraformApplyUnknownAttrStr = ` const testTerraformApplyUnknownAttrStr = `
aws_instance.foo: aws_instance.foo:
ID = foo ID = foo

View File

@ -0,0 +1,12 @@
resource "aws_instance" "foo" {
num = "2"
}
resource "aws_instance" "bar" {
foo = "bar"
count = 3
}
output "foo_num" {
value = "${aws_instance.bar.*.foo}"
}

View File

@ -0,0 +1,11 @@
resource "aws_instance" "foo" {
num = "2"
}
resource "aws_instance" "bar" {
foo = "bar"
}
output "foo_num" {
value = "${aws_instance.foo.num}"
}