commit
41eceedc4c
102
config/config.go
102
config/config.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}"
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
resource "aws_instance" "web" {
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ip" {
|
||||||
|
value = "foo"
|
||||||
|
another = "nope"
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
resource "aws_instance" "web" {
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ip" {
|
||||||
|
value = "${aws_instance.loadbalancer.foo}"
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}"
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
resource "aws_instance" "foo" {
|
||||||
|
num = "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "bar" {
|
||||||
|
foo = "bar"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "foo_num" {
|
||||||
|
value = "${aws_instance.foo.num}"
|
||||||
|
}
|
Loading…
Reference in New Issue