Merge branch 'master' into expand-user-path

This commit is contained in:
Alex Gaynor 2014-08-19 12:19:16 -07:00
commit f48b888477
73 changed files with 5339 additions and 1056 deletions

View File

@ -1,8 +1,31 @@
## 0.2.0 (unreleased)
BACKWARDS INCOMPATIBILITIES:
* We've replaced the configuration language in use from a C library to
a pure-Go reimplementation. In the process, we removed some features
of the language since it was too flexible:
* Semicolons are no longer valid at the end of lines
* Keys cannot be double-quoted strings: `"foo" = "bar"` is no longer
valid.
IMPROVEMENTS:
* core: "~/.terraformrc" (Unix) or "%APPDATA%/terraform.rc" (Windows)
can be used to configure custom providers and provisioners. [GH-192]
BUG FIXES:
* core: Variables are validated to not contain interpolations. [GH-180]
* providers/heroku: If you delete the `config_vars` block, config vars
are properly nuked.
* providers/heroku: Domains and drains are deleted before the app.
PLUGIN CHANGES:
* **New Package:** `helper/schema`. This introduces a high-level framework
for easily writing new providers and resources. The Heroku provider has
been converted to this as an example.
## 0.1.1 (August 5, 2014)

View File

@ -1,69 +1,34 @@
CGO_CFLAGS:=-I$(CURDIR)/vendor/libucl/include
CGO_LDFLAGS:=-L$(CURDIR)/vendor/libucl
LIBUCL_NAME=libucl.a
TEST?=./...
# Windows-only
ifeq ($(OS), Windows_NT)
# The Libucl library is named libucl.dll
LIBUCL_NAME=libucl.dll
# Add the current directory on the path so the DLL is available.
export PATH := $(CURDIR):$(PATH)
endif
export CGO_CFLAGS CGO_LDFLAGS PATH
default: test
bin: config/y.go libucl
bin: config/y.go
@sh -c "$(CURDIR)/scripts/build.sh"
dev: config/y.go libucl
dev: config/y.go
@TF_DEV=1 sh -c "$(CURDIR)/scripts/build.sh"
libucl: vendor/libucl/$(LIBUCL_NAME)
test: config/y.go libucl
test: config/y.go
TF_ACC= go test $(TEST) $(TESTARGS) -timeout=10s
testacc: config/y.go libucl
testacc: config/y.go
@if [ "$(TEST)" = "./..." ]; then \
echo "ERROR: Set TEST to a specific package"; \
exit 1; \
fi
TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 30m
testrace: config/y.go libucl
testrace: config/y.go
TF_ACC= go test -race $(TEST) $(TESTARGS)
updatedeps: config/y.go libucl
updatedeps: config/y.go
go get -u -v ./...
config/y.go: config/expr.y
cd config/ && \
go tool yacc -p "expr" expr.y
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/hashicorp/libucl.git vendor/libucl
cd vendor/libucl && \
git checkout fix-win32-compile
clean:
rm config/y.go
rm -rf vendor
.PHONY: clean default libucl test updatedeps
.PHONY: clean default test updatedeps

View File

@ -48,15 +48,12 @@ All documentation is available on the
If you wish to work on Terraform itself or any of its built-in providers,
you'll first need [Go](http://www.golang.org) installed (version 1.2+ is
_required_). Make sure Go is properly installed, including setting up
a [GOPATH](http://golang.org/doc/code.html#GOPATH). Make sure Go is compiled
with cgo support. You can verify this by running `go env` and checking that
`CGOENABLED` is set to "1".
a [GOPATH](http://golang.org/doc/code.html#GOPATH).
Next, install the following software packages, which are needed for some dependencies:
- [Git](http://git-scm.com/)
- [Mercurial](http://mercurial.selenic.com/)
- [CMake](http://www.cmake.org/)
Then, install [Gox](https://github.com/mitchellh/gox), which is used
as a compilation tool on top of Go:
@ -64,8 +61,8 @@ as a compilation tool on top of Go:
$ go get -u github.com/mitchellh/gox
Next, clone this repository into `$GOPATH/src/github.com/hashicorp/terraform`.
Install the necessary dependencies by running `make updatedeps` and then just
type `make`. This will compile some more dependencies and then run the tests. If
Install the necessary dependencies by running `make updatedeps` and then just
type `make`. This will compile some more dependencies and then run the tests. If
this exits with exit status 0, then everything is working!
$ make updatedeps

View File

@ -1,6 +1,4 @@
This is just to keep track of what we need to do in the short term.
* Configuration schemas for providers and provisioners
* Helper improvements around complex types perhaps
* Providers/AWS: `aws_security_group` needs an update func
* Providers/AWS: `aws_rds` needs an update func (+goamz changes)

View File

@ -6,5 +6,5 @@ import (
)
func main() {
plugin.Serve(new(heroku.ResourceProvider))
plugin.Serve(heroku.Provider())
}

View File

@ -159,6 +159,6 @@ resource "aws_db_instance" "bar" {
skip_final_snapshot = true
security_group_names = [${aws_db_security_group.bar.name}]
security_group_names = ["${aws_db_security_group.bar.name}"]
}
`

View File

@ -43,8 +43,17 @@ func resource_aws_elb_create(
if _, ok := rs.Attributes["availability_zones.#"]; ok {
v = flatmap.Expand(rs.Attributes, "availability_zones").([]interface{})
zones := expandStringList(v)
elbOpts.AvailZone = zones
elbOpts.AvailZone = expandStringList(v)
}
if _, ok := rs.Attributes["security_groups.#"]; ok {
v = flatmap.Expand(rs.Attributes, "security_groups").([]interface{})
elbOpts.SecurityGroups = expandStringList(v)
}
if _, ok := rs.Attributes["subnets.#"]; ok {
v = flatmap.Expand(rs.Attributes, "subnets").([]interface{})
elbOpts.Subnets = expandStringList(v)
}
log.Printf("[DEBUG] ELB create configuration: %#v", elbOpts)
@ -248,6 +257,8 @@ func resource_aws_elb_diff(
Attrs: map[string]diff.AttrType{
"name": diff.AttrTypeCreate,
"availability_zone": diff.AttrTypeCreate,
"security_groups": diff.AttrTypeCreate, // TODO could be AttrTypeUpdate
"subnets": diff.AttrTypeCreate, // TODO could be AttrTypeUpdate
"listener": diff.AttrTypeCreate,
"instances": diff.AttrTypeUpdate,
"health_check": diff.AttrTypeCreate,
@ -275,6 +286,14 @@ func resource_aws_elb_update_state(
toFlatten["instances"] = flattenInstances(balancer.Instances)
}
if len(balancer.SecurityGroups) > 0 && balancer.SecurityGroups[0] != "" {
toFlatten["security_groups"] = balancer.SecurityGroups
}
if len(balancer.Subnets) > 0 && balancer.Subnets[0] != "" {
toFlatten["subnets"] = balancer.Subnets
}
// There's only one health check, so save that to state as we
// currently can
if balancer.HealthCheck.Target != "" {
@ -326,6 +345,8 @@ func resource_aws_elb_validation() *config.Validator {
Optional: []string{
"instances.*",
"availability_zones.*",
"security_groups.*",
"subnets.*",
"health_check.#",
"health_check.0.healthy_threshold",
"health_check.0.unhealthy_threshold",

View File

@ -81,8 +81,13 @@ func expandIPPerms(configured []interface{}) ([]ec2.IPPerm, error) {
gs := expandStringList(secGroups)
for _, g := range gs {
ownerId, id := "", g
if items := strings.Split(g, "/"); len(items) > 1 {
ownerId, id = items[0], items[1]
}
newG := ec2.UserSecurityGroup{
Id: g,
Id: id,
OwnerId: ownerId,
}
expandedGroups = append(expandedGroups, newG)
}

View File

@ -26,8 +26,9 @@ func testConf() map[string]string {
"ingress.0.to_port": "-1",
"ingress.0.cidr_blocks.#": "1",
"ingress.0.cidr_blocks.0": "0.0.0.0/0",
"ingress.0.security_groups.#": "1",
"ingress.0.security_groups.#": "2",
"ingress.0.security_groups.0": "sg-11111",
"ingress.0.security_groups.1": "foo/sg-22222",
}
}
@ -47,6 +48,10 @@ func Test_expandIPPerms(t *testing.T) {
ec2.UserSecurityGroup{
Id: "sg-11111",
},
ec2.UserSecurityGroup{
OwnerId: "foo",
Id: "sg-22222",
},
},
}
@ -92,6 +97,10 @@ func Test_expandIPPerms_NoCidr(t *testing.T) {
ec2.UserSecurityGroup{
Id: "sg-11111",
},
ec2.UserSecurityGroup{
OwnerId: "foo",
Id: "sg-22222",
},
},
}

View File

@ -0,0 +1,45 @@
package heroku
import (
"log"
"github.com/hashicorp/terraform/helper/schema"
"github.com/mitchellh/mapstructure"
)
// Provider returns a terraform.ResourceProvider.
func Provider() *schema.Provider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"email": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"api_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
ResourcesMap: map[string]*schema.Resource{
"heroku_app": resourceHerokuApp(),
"heroku_addon": resourceHerokuAddon(),
"heroku_domain": resourceHerokuDomain(),
"heroku_drain": resourceHerokuDrain(),
},
ConfigureFunc: providerConfigure,
}
}
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
var config Config
configRaw := d.Get("").(map[string]interface{})
if err := mapstructure.Decode(configRaw, &config); err != nil {
return nil, err
}
log.Println("[INFO] Initializing Heroku client")
return config.Client()
}

View File

@ -2,29 +2,35 @@ package heroku
import (
"os"
"reflect"
"testing"
"github.com/bgentry/heroku-go"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
var testAccProviders map[string]terraform.ResourceProvider
var testAccProvider *ResourceProvider
var testAccProvider *schema.Provider
func init() {
testAccProvider = new(ResourceProvider)
testAccProvider = Provider()
testAccProviders = map[string]terraform.ResourceProvider{
"heroku": testAccProvider,
}
}
func TestResourceProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = new(ResourceProvider)
func TestProvider(t *testing.T) {
if err := Provider().InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestResourceProvider_Configure(t *testing.T) {
rp := new(ResourceProvider)
func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = Provider()
}
func TestProviderConfigure(t *testing.T) {
var expectedKey string
var expectedEmail string
@ -50,18 +56,18 @@ func TestResourceProvider_Configure(t *testing.T) {
t.Fatalf("err: %s", err)
}
rp := Provider()
err = rp.Configure(terraform.NewResourceConfig(rawConfig))
if err != nil {
t.Fatalf("err: %s", err)
}
expected := Config{
APIKey: expectedKey,
Email: expectedEmail,
config := rp.Meta().(*heroku.Client)
if config.Username != expectedEmail {
t.Fatalf("bad: %#v", config)
}
if !reflect.DeepEqual(rp.Config, expected) {
t.Fatalf("bad: %#v", rp.Config)
if config.Password != expectedKey {
t.Fatalf("bad: %#v", config)
}
}

View File

@ -6,9 +6,7 @@ import (
"sync"
"github.com/bgentry/heroku-go"
"github.com/hashicorp/terraform/flatmap"
"github.com/hashicorp/terraform/helper/config"
"github.com/hashicorp/terraform/helper/diff"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
@ -17,163 +15,133 @@ import (
// multiple addons simultaneously.
var addonLock sync.Mutex
func resource_heroku_addon_create(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
func resourceHerokuAddon() *schema.Resource {
return &schema.Resource{
Create: resourceHerokuAddonCreate,
Read: resourceHerokuAddonRead,
Update: resourceHerokuAddonUpdate,
Delete: resourceHerokuAddonDelete,
Schema: map[string]*schema.Schema{
"app": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"plan": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"config": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeMap,
},
},
"provider_id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"config_vars": &schema.Schema{
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeMap},
},
},
}
}
func resourceHerokuAddonCreate(d *schema.ResourceData, meta interface{}) error {
addonLock.Lock()
defer addonLock.Unlock()
p := meta.(*ResourceProvider)
client := p.client
client := meta.(*heroku.Client)
// Merge the diff into the state so that we have all the attributes
// properly.
rs := s.MergeDiff(d)
app := rs.Attributes["app"]
plan := rs.Attributes["plan"]
app := d.Get("app").(string)
plan := d.Get("plan").(string)
opts := heroku.AddonCreateOpts{}
if attr, ok := rs.Attributes["config.#"]; ok && attr == "1" {
vs := flatmap.Expand(
rs.Attributes, "config").([]interface{})
if v := d.Get("config"); v != nil {
config := make(map[string]string)
for k, v := range vs[0].(map[string]interface{}) {
config[k] = v.(string)
for _, v := range v.([]interface{}) {
for k, v := range v.(map[string]interface{}) {
config[k] = v.(string)
}
}
opts.Config = &config
}
log.Printf("[DEBUG] Addon create configuration: %#v, %#v, %#v", app, plan, opts)
a, err := client.AddonCreate(app, plan, &opts)
if err != nil {
return s, err
return err
}
rs.ID = a.Id
log.Printf("[INFO] Addon ID: %s", rs.ID)
d.SetId(a.Id)
log.Printf("[INFO] Addon ID: %s", d.Id())
addon, err := resource_heroku_addon_retrieve(app, rs.ID, client)
if err != nil {
return rs, err
}
return resource_heroku_addon_update_state(rs, addon)
return resourceHerokuAddonRead(d, meta)
}
func resource_heroku_addon_update(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
rs := s.MergeDiff(d)
app := rs.Attributes["app"]
if attr, ok := d.Attributes["plan"]; ok {
ad, err := client.AddonUpdate(
app, rs.ID,
attr.New)
if err != nil {
return s, err
}
// Store the new ID
rs.ID = ad.Id
}
addon, err := resource_heroku_addon_retrieve(app, rs.ID, client)
func resourceHerokuAddonRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*heroku.Client)
addon, err := resource_heroku_addon_retrieve(
d.Get("app").(string), d.Id(), client)
if err != nil {
return rs, err
return err
}
return resource_heroku_addon_update_state(rs, addon)
}
func resource_heroku_addon_destroy(
s *terraform.ResourceState,
meta interface{}) error {
p := meta.(*ResourceProvider)
client := p.client
log.Printf("[INFO] Deleting Addon: %s", s.ID)
// Destroy the app
err := client.AddonDelete(s.Attributes["app"], s.ID)
if err != nil {
return fmt.Errorf("Error deleting addon: %s", err)
}
d.Set("name", addon.Name)
d.Set("plan", addon.Plan.Name)
d.Set("provider_id", addon.ProviderId)
d.Set("config_vars", []interface{}{addon.ConfigVars})
d.SetDependencies([]terraform.ResourceDependency{
terraform.ResourceDependency{ID: d.Get("app").(string)},
})
return nil
}
func resource_heroku_addon_refresh(
s *terraform.ResourceState,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
func resourceHerokuAddonUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*heroku.Client)
app, err := resource_heroku_addon_retrieve(s.Attributes["app"], s.ID, client)
app := d.Get("app").(string)
if d.HasChange("plan") {
ad, err := client.AddonUpdate(
app, d.Id(), d.Get("plan").(string))
if err != nil {
return err
}
// Store the new ID
d.SetId(ad.Id)
}
return resourceHerokuAddonRead(d, meta)
}
func resourceHerokuAddonDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*heroku.Client)
log.Printf("[INFO] Deleting Addon: %s", d.Id())
// Destroy the app
err := client.AddonDelete(d.Get("app").(string), d.Id())
if err != nil {
return nil, err
return fmt.Errorf("Error deleting addon: %s", err)
}
return resource_heroku_addon_update_state(s, app)
}
func resource_heroku_addon_diff(
s *terraform.ResourceState,
c *terraform.ResourceConfig,
meta interface{}) (*terraform.ResourceDiff, error) {
b := &diff.ResourceBuilder{
Attrs: map[string]diff.AttrType{
"app": diff.AttrTypeCreate,
"plan": diff.AttrTypeUpdate,
"config": diff.AttrTypeCreate,
},
ComputedAttrs: []string{
"provider_id",
"config_vars",
},
}
return b.Diff(s, c)
}
func resource_heroku_addon_update_state(
s *terraform.ResourceState,
addon *heroku.Addon) (*terraform.ResourceState, error) {
s.Attributes["name"] = addon.Name
s.Attributes["plan"] = addon.Plan.Name
s.Attributes["provider_id"] = addon.ProviderId
toFlatten := make(map[string]interface{})
if len(addon.ConfigVars) > 0 {
toFlatten["config_vars"] = addon.ConfigVars
}
for k, v := range flatmap.Flatten(toFlatten) {
s.Attributes[k] = v
}
s.Dependencies = []terraform.ResourceDependency{
terraform.ResourceDependency{ID: s.Attributes["app"]},
}
return s, nil
d.SetId("")
return nil
}
func resource_heroku_addon_retrieve(app string, id string, client *heroku.Client) (*heroku.Addon, error) {
@ -185,15 +153,3 @@ func resource_heroku_addon_retrieve(app string, id string, client *heroku.Client
return addon, nil
}
func resource_heroku_addon_validation() *config.Validator {
return &config.Validator{
Required: []string{
"app",
"plan",
},
Optional: []string{
"config.*",
},
}
}

View File

@ -35,7 +35,7 @@ func TestAccHerokuAddon_Basic(t *testing.T) {
}
func testAccCheckHerokuAddonDestroy(s *terraform.State) error {
client := testAccProvider.client
client := testAccProvider.Meta().(*heroku.Client)
for _, rs := range s.Resources {
if rs.Type != "heroku_addon" {
@ -75,7 +75,7 @@ func testAccCheckHerokuAddonExists(n string, addon *heroku.Addon) resource.TestC
return fmt.Errorf("No Addon ID is set")
}
client := testAccProvider.client
client := testAccProvider.Meta().(*heroku.Client)
foundAddon, err := client.AddonInfo(rs.Attributes["app"], rs.ID)

View File

@ -5,11 +5,8 @@ import (
"log"
"github.com/bgentry/heroku-go"
"github.com/hashicorp/terraform/flatmap"
"github.com/hashicorp/terraform/helper/config"
"github.com/hashicorp/terraform/helper/diff"
"github.com/hashicorp/terraform/helper/multierror"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/helper/schema"
)
// type application is used to store all the details of a heroku app
@ -43,200 +40,172 @@ func (a *application) Update() error {
return nil
}
func resource_heroku_app_create(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
func resourceHerokuApp() *schema.Resource {
return &schema.Resource{
Create: resourceHerokuAppCreate,
Read: resourceHerokuAppRead,
Update: resourceHerokuAppUpdate,
Delete: resourceHerokuAppDelete,
// Merge the diff into the state so that we have all the attributes
// properly.
rs := s.MergeDiff(d)
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"region": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"stack": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"config_vars": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeMap,
},
},
"git_url": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"web_url": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"heroku_hostname": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}
func resourceHerokuAppCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*heroku.Client)
// Build up our creation options
opts := heroku.AppCreateOpts{}
if attr := rs.Attributes["name"]; attr != "" {
opts.Name = &attr
if v := d.Get("name"); v != nil {
vs := v.(string)
log.Printf("[DEBUG] App name: %s", vs)
opts.Name = &vs
}
if v := d.Get("region"); v != nil {
vs := v.(string)
log.Printf("[DEBUG] App region: %s", vs)
opts.Region = &vs
}
if v := d.Get("stack"); v != nil {
vs := v.(string)
log.Printf("[DEBUG] App stack: %s", vs)
opts.Stack = &vs
}
if attr := rs.Attributes["region"]; attr != "" {
opts.Region = &attr
}
if attr := rs.Attributes["stack"]; attr != "" {
opts.Stack = &attr
}
log.Printf("[DEBUG] App create configuration: %#v", opts)
log.Printf("[DEBUG] Creating Heroku app...")
a, err := client.AppCreate(&opts)
if err != nil {
return s, err
return err
}
rs.ID = a.Name
log.Printf("[INFO] App ID: %s", rs.ID)
d.SetId(a.Name)
log.Printf("[INFO] App ID: %s", d.Id())
if attr, ok := rs.Attributes["config_vars.#"]; ok && attr == "1" {
vs := flatmap.Expand(
rs.Attributes, "config_vars").([]interface{})
err = update_config_vars(rs.ID, vs, client)
if v := d.Get("config_vars"); v != nil {
err = update_config_vars(d.Id(), client, nil, v.([]interface{}))
if err != nil {
return rs, err
return err
}
}
app, err := resource_heroku_app_retrieve(rs.ID, client)
if err != nil {
return rs, err
}
return resource_heroku_app_update_state(rs, app)
return resourceHerokuAppRead(d, meta)
}
func resource_heroku_app_update(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
rs := s.MergeDiff(d)
if attr, ok := d.Attributes["name"]; ok {
opts := heroku.AppUpdateOpts{
Name: &attr.New,
}
renamedApp, err := client.AppUpdate(rs.ID, &opts)
if err != nil {
return s, err
}
// Store the new ID
rs.ID = renamedApp.Name
}
attr, ok := s.Attributes["config_vars.#"]
// If the config var block was removed, nuke all config vars
if ok && attr == "1" {
vs := flatmap.Expand(
rs.Attributes, "config_vars").([]interface{})
err := update_config_vars(rs.ID, vs, client)
if err != nil {
return rs, err
}
} else if ok && attr == "0" {
log.Println("[INFO] Config vars removed, removing all vars")
err := update_config_vars(rs.ID, make([]interface{}, 0), client)
if err != nil {
return rs, err
}
}
app, err := resource_heroku_app_retrieve(rs.ID, client)
func resourceHerokuAppRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*heroku.Client)
app, err := resource_heroku_app_retrieve(d.Id(), client)
if err != nil {
return rs, err
return err
}
return resource_heroku_app_update_state(rs, app)
}
d.Set("name", app.App.Name)
d.Set("stack", app.App.Stack.Name)
d.Set("region", app.App.Region.Name)
d.Set("git_url", app.App.GitURL)
d.Set("web_url", app.App.WebURL)
d.Set("config_vars", []map[string]string{app.Vars})
func resource_heroku_app_destroy(
s *terraform.ResourceState,
meta interface{}) error {
p := meta.(*ResourceProvider)
client := p.client
log.Printf("[INFO] Deleting App: %s", s.ID)
// Destroy the app
err := client.AppDelete(s.ID)
if err != nil {
return fmt.Errorf("Error deleting App: %s", err)
}
// We know that the hostname on heroku will be the name+herokuapp.com
// You need this to do things like create DNS CNAME records
d.Set("heroku_hostname", fmt.Sprintf("%s.herokuapp.com", app.App.Name))
return nil
}
func resource_heroku_app_refresh(
s *terraform.ResourceState,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
func resourceHerokuAppUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*heroku.Client)
app, err := resource_heroku_app_retrieve(s.ID, client)
// If name changed, update it
if d.HasChange("name") {
v := d.Get("name").(string)
opts := heroku.AppUpdateOpts{
Name: &v,
}
renamedApp, err := client.AppUpdate(d.Id(), &opts)
if err != nil {
return err
}
// Store the new ID
d.SetId(renamedApp.Name)
}
// If the config vars changed, then recalculate those
if d.HasChange("config_vars") {
o, n := d.GetChange("config_vars")
if o == nil {
o = []interface{}{}
}
if n == nil {
n = []interface{}{}
}
err := update_config_vars(
d.Id(), client, o.([]interface{}), n.([]interface{}))
if err != nil {
return err
}
}
return resourceHerokuAppRead(d, meta)
}
func resourceHerokuAppDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*heroku.Client)
log.Printf("[INFO] Deleting App: %s", d.Id())
err := client.AppDelete(d.Id())
if err != nil {
return nil, err
return fmt.Errorf("Error deleting App: %s", err)
}
return resource_heroku_app_update_state(s, app)
}
func resource_heroku_app_diff(
s *terraform.ResourceState,
c *terraform.ResourceConfig,
meta interface{}) (*terraform.ResourceDiff, error) {
b := &diff.ResourceBuilder{
Attrs: map[string]diff.AttrType{
"name": diff.AttrTypeUpdate,
"region": diff.AttrTypeUpdate,
"stack": diff.AttrTypeCreate,
"config_vars": diff.AttrTypeUpdate,
},
ComputedAttrs: []string{
"name",
"region",
"stack",
"git_url",
"web_url",
"id",
"config_vars",
},
ComputedAttrsUpdate: []string{
"heroku_hostname",
},
}
return b.Diff(s, c)
}
func resource_heroku_app_update_state(
s *terraform.ResourceState,
app *application) (*terraform.ResourceState, error) {
s.Attributes["name"] = app.App.Name
s.Attributes["stack"] = app.App.Stack.Name
s.Attributes["region"] = app.App.Region.Name
s.Attributes["git_url"] = app.App.GitURL
s.Attributes["web_url"] = app.App.WebURL
// We know that the hostname on heroku will be the name+herokuapp.com
// You need this to do things like create DNS CNAME records
s.Attributes["heroku_hostname"] = fmt.Sprintf("%s.herokuapp.com", app.App.Name)
toFlatten := make(map[string]interface{})
if len(app.Vars) > 0 {
toFlatten["config_vars"] = []map[string]string{app.Vars}
}
for k, v := range flatmap.Flatten(toFlatten) {
s.Attributes[k] = v
}
return s, nil
d.SetId("")
return nil
}
func resource_heroku_app_retrieve(id string, client *heroku.Client) (*application, error) {
@ -251,18 +220,6 @@ func resource_heroku_app_retrieve(id string, client *heroku.Client) (*applicatio
return &app, nil
}
func resource_heroku_app_validation() *config.Validator {
return &config.Validator{
Required: []string{},
Optional: []string{
"name",
"region",
"stack",
"config_vars.*",
},
}
}
func retrieve_config_vars(id string, client *heroku.Client) (map[string]string, error) {
vars, err := client.ConfigVarInfo(id)
@ -273,21 +230,28 @@ func retrieve_config_vars(id string, client *heroku.Client) (map[string]string,
return vars, nil
}
// Updates the config vars for from an expanded (prior to assertion)
// []map[string]string config
func update_config_vars(id string, vs []interface{}, client *heroku.Client) error {
// Updates the config vars for from an expanded configuration.
func update_config_vars(
id string,
client *heroku.Client,
o []interface{},
n []interface{}) error {
vars := make(map[string]*string)
for k, v := range vs[0].(map[string]interface{}) {
val := v.(string)
vars[k] = &val
for _, v := range o {
for k, _ := range v.(map[string]interface{}) {
vars[k] = nil
}
}
for _, v := range n {
for k, v := range v.(map[string]interface{}) {
val := v.(string)
vars[k] = &val
}
}
log.Printf("[INFO] Updating config vars: *%#v", vars)
_, err := client.ConfigVarUpdate(id, vars)
if err != nil {
if _, err := client.ConfigVarUpdate(id, vars); err != nil {
return fmt.Errorf("Error updating config vars: %s", err)
}

View File

@ -103,7 +103,7 @@ func TestAccHerokuApp_NukeVars(t *testing.T) {
}
func testAccCheckHerokuAppDestroy(s *terraform.State) error {
client := testAccProvider.client
client := testAccProvider.Meta().(*heroku.Client)
for _, rs := range s.Resources {
if rs.Type != "heroku_app" {
@ -122,7 +122,7 @@ func testAccCheckHerokuAppDestroy(s *terraform.State) error {
func testAccCheckHerokuAppAttributes(app *heroku.App) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testAccProvider.client
client := testAccProvider.Meta().(*heroku.Client)
if app.Region.Name != "us" {
return fmt.Errorf("Bad region: %s", app.Region.Name)
@ -151,7 +151,7 @@ func testAccCheckHerokuAppAttributes(app *heroku.App) resource.TestCheckFunc {
func testAccCheckHerokuAppAttributesUpdated(app *heroku.App) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testAccProvider.client
client := testAccProvider.Meta().(*heroku.Client)
if app.Name != "terraform-test-renamed" {
return fmt.Errorf("Bad name: %s", app.Name)
@ -178,7 +178,7 @@ func testAccCheckHerokuAppAttributesUpdated(app *heroku.App) resource.TestCheckF
func testAccCheckHerokuAppAttributesNoVars(app *heroku.App) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testAccProvider.client
client := testAccProvider.Meta().(*heroku.Client)
if app.Name != "terraform-test-app" {
return fmt.Errorf("Bad name: %s", app.Name)
@ -209,7 +209,7 @@ func testAccCheckHerokuAppExists(n string, app *heroku.App) resource.TestCheckFu
return fmt.Errorf("No App Name is set")
}
client := testAccProvider.client
client := testAccProvider.Meta().(*heroku.Client)
foundApp, err := client.AppInfo(rs.ID)

View File

@ -5,63 +5,68 @@ import (
"log"
"github.com/bgentry/heroku-go"
"github.com/hashicorp/terraform/helper/config"
"github.com/hashicorp/terraform/helper/diff"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
func resource_heroku_domain_create(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
func resourceHerokuDomain() *schema.Resource {
return &schema.Resource{
Create: resourceHerokuDomainCreate,
Read: resourceHerokuDomainRead,
Delete: resourceHerokuDomainDelete,
// Merge the diff into the state so that we have all the attributes
// properly.
rs := s.MergeDiff(d)
Schema: map[string]*schema.Schema{
"hostname": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
app := rs.Attributes["app"]
hostname := rs.Attributes["hostname"]
"app": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"cname": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}
func resourceHerokuDomainCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*heroku.Client)
app := d.Get("app").(string)
hostname := d.Get("hostname").(string)
log.Printf("[DEBUG] Domain create configuration: %#v, %#v", app, hostname)
do, err := client.DomainCreate(app, hostname)
if err != nil {
return s, err
return err
}
rs.ID = do.Id
rs.Attributes["hostname"] = do.Hostname
rs.Attributes["cname"] = fmt.Sprintf("%s.herokuapp.com", app)
d.SetId(do.Id)
d.Set("hostname", do.Hostname)
d.Set("cname", fmt.Sprintf("%s.herokuapp.com", app))
d.SetDependencies([]terraform.ResourceDependency{
terraform.ResourceDependency{ID: app},
})
log.Printf("[INFO] Domain ID: %s", rs.ID)
return rs, nil
log.Printf("[INFO] Domain ID: %s", d.Id())
return nil
}
func resource_heroku_domain_update(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
func resourceHerokuDomainDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*heroku.Client)
panic("Cannot update domain")
return nil, nil
}
func resource_heroku_domain_destroy(
s *terraform.ResourceState,
meta interface{}) error {
p := meta.(*ResourceProvider)
client := p.client
log.Printf("[INFO] Deleting Domain: %s", s.ID)
// Destroy the app
err := client.DomainDelete(s.Attributes["app"], s.ID)
log.Printf("[INFO] Deleting Domain: %s", d.Id())
// Destroy the domain
err := client.DomainDelete(d.Get("app").(string), d.Id())
if err != nil {
return fmt.Errorf("Error deleting domain: %s", err)
}
@ -69,58 +74,17 @@ func resource_heroku_domain_destroy(
return nil
}
func resource_heroku_domain_refresh(
s *terraform.ResourceState,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
func resourceHerokuDomainRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*heroku.Client)
domain, err := resource_heroku_domain_retrieve(s.Attributes["app"], s.ID, client)
app := d.Get("app").(string)
do, err := client.DomainInfo(app, d.Id())
if err != nil {
return nil, err
return fmt.Errorf("Error retrieving domain: %s", err)
}
s.Attributes["hostname"] = domain.Hostname
s.Attributes["cname"] = fmt.Sprintf("%s.herokuapp.com", s.Attributes["app"])
d.Set("hostname", do.Hostname)
d.Set("cname", fmt.Sprintf("%s.herokuapp.com", app))
return s, nil
}
func resource_heroku_domain_diff(
s *terraform.ResourceState,
c *terraform.ResourceConfig,
meta interface{}) (*terraform.ResourceDiff, error) {
b := &diff.ResourceBuilder{
Attrs: map[string]diff.AttrType{
"hostname": diff.AttrTypeCreate,
"app": diff.AttrTypeCreate,
},
ComputedAttrs: []string{
"cname",
},
}
return b.Diff(s, c)
}
func resource_heroku_domain_retrieve(app string, id string, client *heroku.Client) (*heroku.Domain, error) {
domain, err := client.DomainInfo(app, id)
if err != nil {
return nil, fmt.Errorf("Error retrieving domain: %s", err)
}
return domain, nil
}
func resource_heroku_domain_validation() *config.Validator {
return &config.Validator{
Required: []string{
"hostname",
"app",
},
Optional: []string{},
}
return nil
}

View File

@ -35,7 +35,7 @@ func TestAccHerokuDomain_Basic(t *testing.T) {
}
func testAccCheckHerokuDomainDestroy(s *terraform.State) error {
client := testAccProvider.client
client := testAccProvider.Meta().(*heroku.Client)
for _, rs := range s.Resources {
if rs.Type != "heroku_domain" {
@ -75,7 +75,7 @@ func testAccCheckHerokuDomainExists(n string, Domain *heroku.Domain) resource.Te
return fmt.Errorf("No Domain ID is set")
}
client := testAccProvider.client
client := testAccProvider.Meta().(*heroku.Client)
foundDomain, err := client.DomainInfo(rs.Attributes["app"], rs.ID)

View File

@ -5,63 +5,68 @@ import (
"log"
"github.com/bgentry/heroku-go"
"github.com/hashicorp/terraform/helper/config"
"github.com/hashicorp/terraform/helper/diff"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
func resource_heroku_drain_create(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
func resourceHerokuDrain() *schema.Resource {
return &schema.Resource{
Create: resourceHerokuDrainCreate,
Read: resourceHerokuDrainRead,
Delete: resourceHerokuDrainDelete,
// Merge the diff into the state so that we have all the attributes
// properly.
rs := s.MergeDiff(d)
Schema: map[string]*schema.Schema{
"url": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
app := rs.Attributes["app"]
url := rs.Attributes["url"]
"app": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"token": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}
func resourceHerokuDrainCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*heroku.Client)
app := d.Get("app").(string)
url := d.Get("url").(string)
log.Printf("[DEBUG] Drain create configuration: %#v, %#v", app, url)
dr, err := client.LogDrainCreate(app, url)
if err != nil {
return s, err
return err
}
rs.ID = dr.Id
rs.Attributes["url"] = dr.URL
rs.Attributes["token"] = dr.Token
d.SetId(dr.Id)
d.Set("url", dr.URL)
d.Set("token", dr.Token)
d.SetDependencies([]terraform.ResourceDependency{
terraform.ResourceDependency{ID: app},
})
log.Printf("[INFO] Drain ID: %s", rs.ID)
return rs, nil
log.Printf("[INFO] Drain ID: %s", d.Id())
return nil
}
func resource_heroku_drain_update(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
func resourceHerokuDrainDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*heroku.Client)
panic("Cannot update drain")
return nil, nil
}
func resource_heroku_drain_destroy(
s *terraform.ResourceState,
meta interface{}) error {
p := meta.(*ResourceProvider)
client := p.client
log.Printf("[INFO] Deleting drain: %s", s.ID)
// Destroy the app
err := client.LogDrainDelete(s.Attributes["app"], s.ID)
log.Printf("[INFO] Deleting drain: %s", d.Id())
// Destroy the drain
err := client.LogDrainDelete(d.Get("app").(string), d.Id())
if err != nil {
return fmt.Errorf("Error deleting drain: %s", err)
}
@ -69,58 +74,16 @@ func resource_heroku_drain_destroy(
return nil
}
func resource_heroku_drain_refresh(
s *terraform.ResourceState,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
func resourceHerokuDrainRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*heroku.Client)
drain, err := resource_heroku_drain_retrieve(s.Attributes["app"], s.ID, client)
dr, err := client.LogDrainInfo(d.Get("app").(string), d.Id())
if err != nil {
return nil, err
return fmt.Errorf("Error retrieving drain: %s", err)
}
s.Attributes["url"] = drain.URL
s.Attributes["token"] = drain.Token
d.Set("url", dr.URL)
d.Set("token", dr.Token)
return s, nil
}
func resource_heroku_drain_diff(
s *terraform.ResourceState,
c *terraform.ResourceConfig,
meta interface{}) (*terraform.ResourceDiff, error) {
b := &diff.ResourceBuilder{
Attrs: map[string]diff.AttrType{
"url": diff.AttrTypeCreate,
"app": diff.AttrTypeCreate,
},
ComputedAttrs: []string{
"token",
},
}
return b.Diff(s, c)
}
func resource_heroku_drain_retrieve(app string, id string, client *heroku.Client) (*heroku.LogDrain, error) {
drain, err := client.LogDrainInfo(app, id)
if err != nil {
return nil, fmt.Errorf("Error retrieving drain: %s", err)
}
return drain, nil
}
func resource_heroku_drain_validation() *config.Validator {
return &config.Validator{
Required: []string{
"url",
"app",
},
Optional: []string{},
}
return nil
}

View File

@ -33,7 +33,7 @@ func TestAccHerokuDrain_Basic(t *testing.T) {
}
func testAccCheckHerokuDrainDestroy(s *terraform.State) error {
client := testAccProvider.client
client := testAccProvider.Meta().(*heroku.Client)
for _, rs := range s.Resources {
if rs.Type != "heroku_drain" {
@ -77,7 +77,7 @@ func testAccCheckHerokuDrainExists(n string, Drain *heroku.LogDrain) resource.Te
return fmt.Errorf("No Drain ID is set")
}
client := testAccProvider.client
client := testAccProvider.Meta().(*heroku.Client)
foundDrain, err := client.LogDrainInfo(rs.Attributes["app"], rs.ID)

View File

@ -1,68 +0,0 @@
package heroku
import (
"log"
"github.com/bgentry/heroku-go"
"github.com/hashicorp/terraform/helper/config"
"github.com/hashicorp/terraform/terraform"
)
type ResourceProvider struct {
Config Config
client *heroku.Client
}
func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
v := &config.Validator{
Required: []string{
"email",
"api_key",
},
}
return v.Validate(c)
}
func (p *ResourceProvider) ValidateResource(
t string, c *terraform.ResourceConfig) ([]string, []error) {
return resourceMap.Validate(t, c)
}
func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error {
if _, err := config.Decode(&p.Config, c.Config); err != nil {
return err
}
log.Println("[INFO] Initializing Heroku client")
var err error
p.client, err = p.Config.Client()
if err != nil {
return err
}
return nil
}
func (p *ResourceProvider) Apply(
s *terraform.ResourceState,
d *terraform.ResourceDiff) (*terraform.ResourceState, error) {
return resourceMap.Apply(s, d, p)
}
func (p *ResourceProvider) Diff(
s *terraform.ResourceState,
c *terraform.ResourceConfig) (*terraform.ResourceDiff, error) {
return resourceMap.Diff(s, c, p)
}
func (p *ResourceProvider) Refresh(
s *terraform.ResourceState) (*terraform.ResourceState, error) {
return resourceMap.Refresh(s, p)
}
func (p *ResourceProvider) Resources() []terraform.ResourceType {
return resourceMap.Resources()
}

View File

@ -1,49 +0,0 @@
package heroku
import (
"github.com/hashicorp/terraform/helper/resource"
)
// resourceMap is the mapping of resources we support to their basic
// operations. This makes it easy to implement new resource types.
var resourceMap *resource.Map
func init() {
resourceMap = &resource.Map{
Mapping: map[string]resource.Resource{
"heroku_addon": resource.Resource{
ConfigValidator: resource_heroku_addon_validation(),
Create: resource_heroku_addon_create,
Destroy: resource_heroku_addon_destroy,
Diff: resource_heroku_addon_diff,
Refresh: resource_heroku_addon_refresh,
Update: resource_heroku_addon_update,
},
"heroku_app": resource.Resource{
ConfigValidator: resource_heroku_app_validation(),
Create: resource_heroku_app_create,
Destroy: resource_heroku_app_destroy,
Diff: resource_heroku_app_diff,
Refresh: resource_heroku_app_refresh,
Update: resource_heroku_app_update,
},
"heroku_domain": resource.Resource{
ConfigValidator: resource_heroku_domain_validation(),
Create: resource_heroku_domain_create,
Destroy: resource_heroku_domain_destroy,
Diff: resource_heroku_domain_diff,
Refresh: resource_heroku_domain_refresh,
},
"heroku_drain": resource.Resource{
ConfigValidator: resource_heroku_drain_validation(),
Create: resource_heroku_drain_create,
Destroy: resource_heroku_drain_destroy,
Diff: resource_heroku_drain_diff,
Refresh: resource_heroku_drain_refresh,
},
},
}
}

View File

@ -2,9 +2,10 @@ package command
import (
"fmt"
"io/ioutil"
"strings"
"github.com/mitchellh/go-libucl"
"github.com/hashicorp/hcl"
)
// FlagVar is a flag.Value implementation for parsing user variables
@ -55,25 +56,23 @@ func (v *FlagVarFile) Set(raw string) error {
return nil
}
const libuclParseFlags = libucl.ParserNoTime
func loadVarFile(path string) (map[string]string, error) {
var obj *libucl.Object
parser := libucl.NewParser(libuclParseFlags)
err := parser.AddFile(path)
if err == nil {
obj = parser.Object()
defer obj.Close()
}
defer parser.Close()
// Read the HCL file and prepare for parsing
d, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
return nil, fmt.Errorf(
"Error reading %s: %s", path, err)
}
// Parse it
obj, err := hcl.Parse(string(d))
if err != nil {
return nil, fmt.Errorf(
"Error parsing %s: %s", path, err)
}
var result map[string]string
if err := obj.Decode(&result); err != nil {
if err := hcl.DecodeObject(&result, obj); err != nil {
return nil, err
}

View File

@ -153,10 +153,12 @@ func (m *Meta) process(args []string, vars bool) []string {
// Set the UI
m.oldUi = m.Ui
m.Ui = &ColorizeUi{
Colorize: m.Colorize(),
ErrorColor: "[red]",
Ui: m.oldUi,
m.Ui = &cli.ConcurrentUi{
Ui: &ColorizeUi{
Colorize: m.Colorize(),
ErrorColor: "[red]",
Ui: m.oldUi,
},
}
// If we support vars and the default var file exists, add it to

View File

@ -1,13 +1,15 @@
package main
import (
"fmt"
"io/ioutil"
"os/exec"
"path/filepath"
"github.com/hashicorp/hcl"
"github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/rpc"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-libucl"
"github.com/mitchellh/osext"
)
@ -27,10 +29,6 @@ var BuiltinConfig Config
// ContextOpts are the global ContextOpts we use to initialize the CLI.
var ContextOpts terraform.ContextOpts
// Put the parse flags we use for libucl in a constant so we can get
// equally behaving parsing everywhere.
const libuclParseFlags = libucl.ParserNoTime
func init() {
BuiltinConfig.Providers = map[string]string{
"aws": "terraform-provider-aws",
@ -47,28 +45,34 @@ func init() {
}
}
// ConfigFile returns the default path to the configuration file.
//
// On Unix-like systems this is the ".terraformrc" file in the home directory.
// On Windows, this is the "terraform.rc" file in the application data
// directory.
func ConfigFile() (string, error) {
return configFile()
}
// LoadConfig loads the CLI configuration from ".terraformrc" files.
func LoadConfig(path string) (*Config, error) {
var obj *libucl.Object
// Parse the file and get the root object.
parser := libucl.NewParser(libuclParseFlags)
err := parser.AddFile(path)
if err == nil {
obj = parser.Object()
defer obj.Close()
}
defer parser.Close()
// If there was an error parsing, return now.
// Read the HCL file and prepare for parsing
d, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
return nil, fmt.Errorf(
"Error reading %s: %s", path, err)
}
// Parse it
obj, err := hcl.Parse(string(d))
if err != nil {
return nil, fmt.Errorf(
"Error parsing %s: %s", path, err)
}
// Build up the result
var result Config
if err := obj.Decode(&result); err != nil {
if err := hcl.DecodeObject(&result, obj); err != nil {
return nil, err
}

View File

@ -127,11 +127,13 @@ func (c *Config) Validate() error {
}
w := &interpolationWalker{F: fn}
if err := reflectwalk.Walk(v.Default, w); err == nil {
if interp {
errs = append(errs, fmt.Errorf(
"Variable '%s': cannot contain interpolations",
v.Name))
if v.Default != nil {
if err := reflectwalk.Walk(v.Default, w); err == nil {
if interp {
errs = append(errs, fmt.Errorf(
"Variable '%s': cannot contain interpolations",
v.Name))
}
}
}
}

View File

@ -161,7 +161,7 @@ func TestVariableDefaultsMap(t *testing.T) {
func testConfig(t *testing.T, name string) *Config {
c, err := Load(filepath.Join(fixtureDir, name, "main.tf"))
if err != nil {
t.Fatalf("err: %s", err)
t.Fatalf("file: %s\n\nerr: %s", name, err)
}
return c

View File

@ -36,7 +36,7 @@ func loadTree(root string) (*importTree, error) {
case ".tf":
fallthrough
case ".tf.json":
f = loadFileLibucl
f = loadFileHcl
default:
}

View File

@ -2,26 +2,20 @@ package config
import (
"fmt"
"path/filepath"
"io/ioutil"
"github.com/mitchellh/go-libucl"
"github.com/hashicorp/hcl"
hclobj "github.com/hashicorp/hcl/hcl"
)
// Put the parse flags we use for libucl in a constant so we can get
// equally behaving parsing everywhere.
const libuclParseFlags = libucl.ParserNoTime
// libuclConfigurable is an implementation of configurable that knows
// how to turn libucl configuration into a *Config object.
type libuclConfigurable struct {
Object *libucl.Object
// hclConfigurable is an implementation of configurable that knows
// how to turn HCL configuration into a *Config object.
type hclConfigurable struct {
File string
Object *hclobj.Object
}
func (t *libuclConfigurable) Close() error {
return t.Object.Close()
}
func (t *libuclConfigurable) Config() (*Config, error) {
func (t *hclConfigurable) Config() (*Config, error) {
validKeys := map[string]struct{}{
"output": struct{}{},
"provider": struct{}{},
@ -29,24 +23,24 @@ func (t *libuclConfigurable) Config() (*Config, error) {
"variable": struct{}{},
}
type LibuclVariable struct {
type hclVariable struct {
Default interface{}
Description string
Fields []string `libucl:",decodedFields"`
Fields []string `hcl:",decodedFields"`
}
var rawConfig struct {
Variable map[string]*LibuclVariable
Variable map[string]*hclVariable
}
if err := t.Object.Decode(&rawConfig); err != nil {
if err := hcl.DecodeObject(&rawConfig, t.Object); err != nil {
return nil, err
}
// Start building up the actual configuration. We start with
// variables.
// TODO(mitchellh): Make function like loadVariablesLibucl so that
// duplicates aren't overridden
// TODO(mitchellh): Make function like loadVariablesHcl so that
// duplicates aren't overriden
config := new(Config)
if len(rawConfig.Variable) > 0 {
config.Variables = make([]*Variable, 0, len(rawConfig.Variable))
@ -76,44 +70,35 @@ func (t *libuclConfigurable) Config() (*Config, error) {
}
// Build the provider configs
providers := t.Object.Get("provider")
if providers != nil {
if providers := t.Object.Get("provider", false); providers != nil {
var err error
config.ProviderConfigs, err = loadProvidersLibucl(providers)
providers.Close()
config.ProviderConfigs, err = loadProvidersHcl(providers)
if err != nil {
return nil, err
}
}
// Build the resources
resources := t.Object.Get("resource")
if resources != nil {
if resources := t.Object.Get("resource", false); resources != nil {
var err error
config.Resources, err = loadResourcesLibucl(resources)
resources.Close()
config.Resources, err = loadResourcesHcl(resources)
if err != nil {
return nil, err
}
}
// Build the outputs
if outputs := t.Object.Get("output"); outputs != nil {
if outputs := t.Object.Get("output", false); outputs != nil {
var err error
config.Outputs, err = loadOutputsLibucl(outputs)
outputs.Close()
config.Outputs, err = loadOutputsHcl(outputs)
if err != nil {
return nil, err
}
}
// Check for invalid keys
iter := t.Object.Iterate(true)
defer iter.Close()
for o := iter.Next(); o != nil; o = iter.Next() {
k := o.Key()
o.Close()
for _, elem := range t.Object.Elem(true) {
k := elem.Key
if _, ok := validKeys[k]; ok {
continue
}
@ -124,91 +109,87 @@ func (t *libuclConfigurable) Config() (*Config, error) {
return config, nil
}
// loadFileLibucl is a fileLoaderFunc that knows how to read libucl
// files and turn them into libuclConfigurables.
func loadFileLibucl(root string) (configurable, []string, error) {
var obj *libucl.Object = nil
// loadFileHcl is a fileLoaderFunc that knows how to read HCL
// files and turn them into hclConfigurables.
func loadFileHcl(root string) (configurable, []string, error) {
var obj *hclobj.Object = nil
// Parse and store the object. We don't use a defer here so that
// we clear resources right away rather than stack them up all the
// way through our recursive calls.
parser := libucl.NewParser(libuclParseFlags)
err := parser.AddFile(root)
if err == nil {
obj = parser.Object()
defer obj.Close()
}
parser.Close()
// If there was an error, return early
// Read the HCL file and prepare for parsing
d, err := ioutil.ReadFile(root)
if err != nil {
return nil, nil, err
return nil, nil, fmt.Errorf(
"Error reading %s: %s", root, err)
}
// Parse it
obj, err = hcl.Parse(string(d))
if err != nil {
return nil, nil, fmt.Errorf(
"Error parsing %s: %s", root, err)
}
// Start building the result
result := &libuclConfigurable{
result := &hclConfigurable{
File: root,
Object: obj,
}
// Otherwise, dive in, find the imports.
imports := obj.Get("import")
if imports == nil {
result.Object.Ref()
return result, nil, nil
}
if imports.Type() != libucl.ObjectTypeString {
imports.Close()
return nil, nil, fmt.Errorf(
"Error in %s: all 'import' declarations should be in the format\n"+
"`import \"foo\"` (Got type %s)",
root,
imports.Type())
}
// Gather all the import paths
importPaths := make([]string, 0, imports.Len())
iter := imports.Iterate(false)
for imp := iter.Next(); imp != nil; imp = iter.Next() {
path := imp.ToString()
if !filepath.IsAbs(path) {
// Relative paths are relative to the Terraform file itself
dir := filepath.Dir(root)
path = filepath.Join(dir, path)
// Dive in, find the imports. This is disabled for now since
// imports were removed prior to Terraform 0.1. The code is
// remaining here commented for historical purposes.
/*
imports := obj.Get("import")
if imports == nil {
result.Object.Ref()
return result, nil, nil
}
importPaths = append(importPaths, path)
imp.Close()
}
iter.Close()
imports.Close()
if imports.Type() != libucl.ObjectTypeString {
imports.Close()
result.Object.Ref()
return result, importPaths, nil
return nil, nil, fmt.Errorf(
"Error in %s: all 'import' declarations should be in the format\n"+
"`import \"foo\"` (Got type %s)",
root,
imports.Type())
}
// Gather all the import paths
importPaths := make([]string, 0, imports.Len())
iter := imports.Iterate(false)
for imp := iter.Next(); imp != nil; imp = iter.Next() {
path := imp.ToString()
if !filepath.IsAbs(path) {
// Relative paths are relative to the Terraform file itself
dir := filepath.Dir(root)
path = filepath.Join(dir, path)
}
importPaths = append(importPaths, path)
imp.Close()
}
iter.Close()
imports.Close()
result.Object.Ref()
*/
return result, nil, nil
}
// LoadOutputsLibucl recurses into the given libucl object and turns
// LoadOutputsHcl recurses into the given HCL object and turns
// it into a mapping of outputs.
func loadOutputsLibucl(o *libucl.Object) ([]*Output, error) {
objects := make(map[string]*libucl.Object)
func loadOutputsHcl(os *hclobj.Object) ([]*Output, error) {
objects := make(map[string]*hclobj.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()
for _, o1 := range os.Elem(false) {
for _, o2 := range o1.Elem(true) {
objects[o2.Key] = o2
}
o1.Close()
iter2.Close()
}
iter.Close()
// If we have none, just return nil
if len(objects) == 0 {
return nil, nil
}
@ -218,7 +199,7 @@ func loadOutputsLibucl(o *libucl.Object) ([]*Output, error) {
for n, o := range objects {
var config map[string]interface{}
if err := o.Decode(&config); err != nil {
if err := hcl.DecodeObject(&config, o); err != nil {
return nil, err
}
@ -239,25 +220,18 @@ func loadOutputsLibucl(o *libucl.Object) ([]*Output, error) {
return result, nil
}
// LoadProvidersLibucl recurses into the given libucl object and turns
// LoadProvidersHcl recurses into the given HCL object and turns
// it into a mapping of provider configs.
func loadProvidersLibucl(o *libucl.Object) ([]*ProviderConfig, error) {
objects := make(map[string]*libucl.Object)
func loadProvidersHcl(os *hclobj.Object) ([]*ProviderConfig, error) {
objects := make(map[string]*hclobj.Object)
// Iterate over all the "provider" 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()
for _, o1 := range os.Elem(false) {
for _, o2 := range o1.Elem(true) {
objects[o2.Key] = o2
}
o1.Close()
iter2.Close()
}
iter.Close()
if len(objects) == 0 {
return nil, nil
@ -268,7 +242,7 @@ func loadProvidersLibucl(o *libucl.Object) ([]*ProviderConfig, error) {
for n, o := range objects {
var config map[string]interface{}
if err := o.Decode(&config); err != nil {
if err := hcl.DecodeObject(&config, o); err != nil {
return nil, err
}
@ -289,16 +263,16 @@ func loadProvidersLibucl(o *libucl.Object) ([]*ProviderConfig, error) {
return result, nil
}
// Given a handle to a libucl object, this recurses into the structure
// Given a handle to a HCL object, this recurses into the structure
// and pulls out a list of resources.
//
// The resulting resources may not be unique, but each resource
// represents exactly one resource definition in the libucl configuration.
// represents exactly one resource definition in the HCL configuration.
// We leave it up to another pass to merge them together.
func loadResourcesLibucl(o *libucl.Object) ([]*Resource, error) {
var allTypes []*libucl.Object
func loadResourcesHcl(os *hclobj.Object) ([]*Resource, error) {
var allTypes []*hclobj.Object
// Libucl object iteration is really nasty. Below is likely to make
// HCL 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
@ -308,25 +282,15 @@ func loadResourcesLibucl(o *libucl.Object) ([]*Resource, error) {
//
// 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() {
for _, o1 := range os.Elem(false) {
// Iterate the inner to get the list of types
iter2 := o1.Iterate(true)
for o2 := iter2.Next(); o2 != nil; o2 = iter2.Next() {
for _, o2 := range o1.Elem(true) {
// Iterate all of this type to get _all_ the types
iter3 := o2.Iterate(false)
for o3 := iter3.Next(); o3 != nil; o3 = iter3.Next() {
for _, o3 := range o2.Elem(false) {
allTypes = append(allTypes, o3)
}
o2.Close()
iter3.Close()
}
o1.Close()
iter2.Close()
}
iter.Close()
// Where all the results will go
var result []*Resource
@ -334,21 +298,15 @@ func loadResourcesLibucl(o *libucl.Object) ([]*Resource, error) {
// 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()
for _, obj := range t.Elem(true) {
k := obj.Key
var config map[string]interface{}
if err := r.Decode(&config); err != nil {
if err := hcl.DecodeObject(&config, obj); err != nil {
return nil, fmt.Errorf(
"Error reading config for %s[%s]: %s",
t.Key(),
r.Key(),
t,
k,
err)
}
@ -362,72 +320,67 @@ func loadResourcesLibucl(o *libucl.Object) ([]*Resource, error) {
if err != nil {
return nil, fmt.Errorf(
"Error reading config for %s[%s]: %s",
t.Key(),
r.Key(),
t,
k,
err)
}
// If we have a count, then figure it out
var count int = 1
if o := r.Get("count"); o != nil {
err = o.Decode(&count)
o.Close()
if o := obj.Get("count", false); o != nil {
err = hcl.DecodeObject(&count, o)
if err != nil {
return nil, fmt.Errorf(
"Error parsing count for %s[%s]: %s",
t.Key(),
r.Key(),
err)
}
}
// If we have connection info, then parse those out
var connInfo map[string]interface{}
if conn := r.Get("connection"); conn != nil {
var err error
connInfo, err = loadConnInfoLibucl(conn)
conn.Close()
if err != nil {
return nil, fmt.Errorf(
"Error reading connection info for %s[%s]: %s",
t.Key(),
r.Key(),
t,
k,
err)
}
}
// If we have depends fields, then add those in
var dependsOn []string
if deps := r.Get("depends_on"); deps != nil {
err := deps.Decode(&dependsOn)
deps.Close()
if o := obj.Get("depends_on", false); o != nil {
err := hcl.DecodeObject(&dependsOn, o)
if err != nil {
return nil, fmt.Errorf(
"Error reading depends_on for %s[%s]: %s",
t.Key(),
r.Key(),
t,
k,
err)
}
}
// If we have connection info, then parse those out
var connInfo map[string]interface{}
if o := obj.Get("connection", false); o != nil {
err := hcl.DecodeObject(&connInfo, o)
if err != nil {
return nil, fmt.Errorf(
"Error reading connection info for %s[%s]: %s",
t,
k,
err)
}
}
// If we have provisioners, then parse those out
var provisioners []*Provisioner
if po := r.Get("provisioner"); po != nil {
if os := obj.Get("provisioner", false); os != nil {
var err error
provisioners, err = loadProvisionersLibucl(po, connInfo)
po.Close()
provisioners, err = loadProvisionersHcl(os, connInfo)
if err != nil {
return nil, fmt.Errorf(
"Error reading provisioners for %s[%s]: %s",
t.Key(),
r.Key(),
t,
k,
err)
}
}
result = append(result, &Resource{
Name: r.Key(),
Type: t.Key(),
Name: k,
Type: t.Key,
Count: count,
RawConfig: rawConfig,
Provisioners: provisioners,
@ -439,8 +392,8 @@ func loadResourcesLibucl(o *libucl.Object) ([]*Resource, error) {
return result, nil
}
func loadProvisionersLibucl(o *libucl.Object, connInfo map[string]interface{}) ([]*Provisioner, error) {
pos := make([]*libucl.Object, 0, int(o.Len()))
func loadProvisionersHcl(os *hclobj.Object, connInfo map[string]interface{}) ([]*Provisioner, error) {
pos := make([]*hclobj.Object, 0, int(os.Len()))
// Accumulate all the actual provisioner configuration objects. We
// have to iterate twice here:
@ -460,28 +413,25 @@ func loadProvisionersLibucl(o *libucl.Object, connInfo map[string]interface{}) (
// }
// ]
//
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() {
for _, o1 := range os.Elem(false) {
for _, o2 := range o1.Elem(true) {
pos = append(pos, o2)
}
o1.Close()
iter2.Close()
}
iter.Close()
// Short-circuit if there are no items
if len(pos) == 0 {
return nil, nil
}
result := make([]*Provisioner, 0, len(pos))
for _, po := range pos {
defer po.Close()
var config map[string]interface{}
if err := po.Decode(&config); err != nil {
if err := hcl.DecodeObject(&config, po); err != nil {
return nil, err
}
// Delete the "connection" section, handle separately
// Delete the "connection" section, handle seperately
delete(config, "connection")
rawConfig, err := NewRawConfig(config)
@ -492,17 +442,15 @@ func loadProvisionersLibucl(o *libucl.Object, connInfo map[string]interface{}) (
// Check if we have a provisioner-level connection
// block that overrides the resource-level
var subConnInfo map[string]interface{}
if conn := po.Get("connection"); conn != nil {
var err error
subConnInfo, err = loadConnInfoLibucl(conn)
conn.Close()
if o := po.Get("connection", false); o != nil {
err := hcl.DecodeObject(&subConnInfo, o)
if err != nil {
return nil, err
}
}
// Inherit from the resource connInfo any keys
// that are not explicitly overridden.
// that are not explicitly overriden.
if connInfo != nil && subConnInfo != nil {
for k, v := range connInfo {
if _, ok := subConnInfo[k]; !ok {
@ -520,7 +468,7 @@ func loadProvisionersLibucl(o *libucl.Object, connInfo map[string]interface{}) (
}
result = append(result, &Provisioner{
Type: po.Key(),
Type: po.Key,
RawConfig: rawConfig,
ConnInfo: connRaw,
})
@ -529,10 +477,22 @@ func loadProvisionersLibucl(o *libucl.Object, connInfo map[string]interface{}) (
return result, nil
}
func loadConnInfoLibucl(o *libucl.Object) (map[string]interface{}, error) {
var config map[string]interface{}
if err := o.Decode(&config); err != nil {
return nil, err
/*
func hclObjectMap(os *hclobj.Object) map[string]ast.ListNode {
objects := make(map[string][]*hclobj.Object)
for _, o := range os.Elem(false) {
for _, elem := range o.Elem(true) {
val, ok := objects[elem.Key]
if !ok {
val = make([]*hclobj.Object, 0, 1)
}
val = append(val, elem)
objects[elem.Key] = val
}
}
return config, nil
return objects
}
*/

View File

@ -0,0 +1,9 @@
package config
import (
"testing"
)
func TestHCLConfigurableConfigurable(t *testing.T) {
var _ configurable = new(hclConfigurable)
}

View File

@ -1,14 +0,0 @@
package config
import (
"io"
"testing"
)
func TestLibuclConfigurableCloser(t *testing.T) {
var _ io.Closer = new(libuclConfigurable)
}
func TestLibuclConfigurableConfigurable(t *testing.T) {
var _ configurable = new(libuclConfigurable)
}

View File

@ -47,6 +47,9 @@ func TestLoadBasic(t *testing.T) {
}
func TestLoadBasic_import(t *testing.T) {
// Skip because we disabled importing
t.Skip()
c, err := Load(filepath.Join(fixtureDir, "import.tf"))
if err != nil {
t.Fatalf("err: %s", err)

View File

@ -1,15 +1,15 @@
variable "foo" {
default = "bar";
description = "bar";
default = "bar"
description = "bar"
}
provider "aws" {
access_key = "foo";
secret_key = "bar";
access_key = "foo"
secret_key = "bar"
}
provider "do" {
api_key = "${var.foo}";
api_key = "${var.foo}"
}
resource "aws_security_group" "firewall" {

View File

@ -1,11 +1,11 @@
variable "foo" {
default = "bar";
description = "bar";
default = "bar"
description = "bar"
}
provider "aws" {
access_key = "foo";
secret_key = "bar";
access_key = "foo"
secret_key = "bar"
}
resource "aws_instance" "db" {

View File

@ -1,5 +1,5 @@
provider "do" {
api_key = "${var.foo}";
api_key = "${var.foo}"
}
resource "aws_security_group" "firewall" {

View File

@ -1,6 +1,6 @@
variable "foo" {
default = "bar";
description = "bar";
default = "bar"
description = "bar"
}
resource "aws_instance" "db" {

View File

@ -2,7 +2,7 @@
"resource": {
"aws_instance": {
"web": {
"foo": "bar",
"foo": "bar"
}
}
}

View File

@ -1,11 +1,11 @@
variable "foo" {
default = "bar";
description = "bar";
default = "bar"
description = "bar"
}
provider "aws" {
access_key = "foo";
secret_key = "bar";
access_key = "foo"
secret_key = "bar"
}
resource "aws_instance" "db" {

View File

@ -1,5 +1,5 @@
provider "do" {
api_key = "${var.foo}";
api_key = "${var.foo}"
}
resource "aws_security_group" "firewall" {

View File

@ -1,12 +1,10 @@
import "import/one.tf";
variable "foo" {
default = "bar";
description = "bar";
default = "bar"
description = "bar"
}
provider "aws" {
foo = "bar";
foo = "bar"
}
resource "aws_security_group" "web" {}

View File

@ -1,21 +1,21 @@
variable "foo" {
default = "bar";
description = "bar";
default = "bar"
description = "bar"
}
variable "amis" {
default = {
"east": "foo",
east = "foo"
}
}
provider "aws" {
access_key = "foo";
secret_key = "bar";
access_key = "foo"
secret_key = "bar"
}
provider "do" {
api_key = "${var.foo}";
api_key = "${var.foo}"
}
resource "aws_security_group" "firewall" {

View File

@ -1 +1 @@
what "is this"
what "is this" {}

View File

@ -1,8 +1,8 @@
variable "foo" {
default = "bar";
description = "bar";
default = "bar"
description = "bar"
}
provider "do" {
api_key = "${var.bar}";
api_key = "${var.bar}"
}

View File

@ -4,6 +4,6 @@ variable "foo" {
variable "foo" {
default = {
"foo" = "bar"
foo = "bar"
}
}

45
config_unix.go Normal file
View File

@ -0,0 +1,45 @@
// +build darwin freebsd linux netbsd openbsd
package main
import (
"bytes"
"errors"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
func configFile() (string, error) {
dir, err := configDir()
if err != nil {
return "", err
}
return filepath.Join(dir, ".terraformrc"), nil
}
func configDir() (string, error) {
// First prefer the HOME environmental variable
if home := os.Getenv("HOME"); home != "" {
log.Printf("Detected home directory from env var: %s", home)
return home, nil
}
// If that fails, try the shell
var stdout bytes.Buffer
cmd := exec.Command("sh", "-c", "eval echo ~$USER")
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return "", err
}
result := strings.TrimSpace(stdout.String())
if result == "" {
return "", errors.New("blank output")
}
return result, nil
}

37
config_windows.go Normal file
View File

@ -0,0 +1,37 @@
// +build windows
package main
import (
"path/filepath"
"syscall"
"unsafe"
)
var (
shell = syscall.MustLoadDLL("Shell32.dll")
getFolderPath = shell.MustFindProc("SHGetFolderPathW")
)
const CSIDL_APPDATA = 26
func configFile() (string, error) {
dir, err := configDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "terraform.rc"), nil
}
func configDir() (string, error) {
b := make([]uint16, syscall.MAX_PATH)
// See: http://msdn.microsoft.com/en-us/library/windows/desktop/bb762181(v=vs.85).aspx
r, _, err := getFolderPath.Call(0, CSIDL_APPDATA, 0, 0, uintptr(unsafe.Pointer(&b[0])))
if uint32(r) != 0 {
return "", err
}
return syscall.UTF16ToString(b), nil
}

View File

@ -1,6 +1,8 @@
package depgraph
import (
"fmt"
"github.com/hashicorp/terraform/digraph"
)
@ -31,6 +33,14 @@ func (d *Dependency) Tail() digraph.Node {
return d.Target
}
func (d *Dependency) GoString() string {
return fmt.Sprintf(
"*Dependency{Name: %s, Source: %s, Target: %s}",
d.Name,
d.Source.Name,
d.Target.Name)
}
func (d *Dependency) String() string {
return d.Name
}

View File

@ -1,6 +1,8 @@
package depgraph
import (
"fmt"
"github.com/hashicorp/terraform/digraph"
)
@ -22,6 +24,10 @@ func (n *Noun) Edges() []digraph.Edge {
return edges
}
func (n *Noun) GoString() string {
return fmt.Sprintf("*%#v", *n)
}
func (n *Noun) String() string {
return n.Name
}

11
helper/schema/README.md Normal file
View File

@ -0,0 +1,11 @@
# Terraform Helper Lib: schema
The `schema` package provides a high-level interface for writing resource
providers for Terraform.
If you're writing a resource provider, we recommend you use this package.
The interface exposed by this package is much friendlier than trying to
write to the Terraform API directly. The core Terraform API is low-level
and built for maximum flexibility and control, whereas this library is built
as a framework around that to more easily write common providers.

160
helper/schema/provider.go Normal file
View File

@ -0,0 +1,160 @@
package schema
import (
"errors"
"fmt"
"sort"
"github.com/hashicorp/terraform/terraform"
)
// Provider represents a Resource provider in Terraform, and properly
// implements all of the ResourceProvider API.
//
// This is a friendlier API than the core Terraform ResourceProvider API,
// and is recommended to be used over that.
type Provider struct {
Schema map[string]*Schema
ResourcesMap map[string]*Resource
ConfigureFunc ConfigureFunc
meta interface{}
}
// ConfigureFunc is the function used to configure a Provider.
//
// The interface{} value returned by this function is stored and passed into
// the subsequent resources as the meta parameter.
type ConfigureFunc func(*ResourceData) (interface{}, error)
// InternalValidate should be called to validate the structure
// of the provider.
//
// This should be called in a unit test for any provider to verify
// before release that a provider is properly configured for use with
// this library.
func (p *Provider) InternalValidate() error {
if p == nil {
return errors.New("provider is nil")
}
if err := schemaMap(p.Schema).InternalValidate(); err != nil {
return err
}
for k, r := range p.ResourcesMap {
if err := r.InternalValidate(); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
}
return nil
}
// Meta returns the metadata associated with this provider that was
// returned by the Configure call. It will be nil until Configure is called.
func (p *Provider) Meta() interface{} {
return p.meta
}
// Validate validates the provider configuration against the schema.
func (p *Provider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return schemaMap(p.Schema).Validate(c)
}
// ValidateResource validates the resource configuration against the
// proper schema.
func (p *Provider) ValidateResource(
t string, c *terraform.ResourceConfig) ([]string, []error) {
r, ok := p.ResourcesMap[t]
if !ok {
return nil, []error{fmt.Errorf(
"Provider doesn't support resource: %s", t)}
}
return r.Validate(c)
}
// Configure implementation of terraform.ResourceProvider interface.
func (p *Provider) Configure(c *terraform.ResourceConfig) error {
// No configuration
if p.ConfigureFunc == nil {
return nil
}
sm := schemaMap(p.Schema)
// Get a ResourceData for this configuration. To do this, we actually
// generate an intermediary "diff" although that is never exposed.
diff, err := sm.Diff(nil, c)
if err != nil {
return err
}
data, err := sm.Data(nil, diff)
if err != nil {
return err
}
meta, err := p.ConfigureFunc(data)
if err != nil {
return err
}
p.meta = meta
return nil
}
// Apply implementation of terraform.ResourceProvider interface.
func (p *Provider) Apply(
s *terraform.ResourceState,
d *terraform.ResourceDiff) (*terraform.ResourceState, error) {
r, ok := p.ResourcesMap[s.Type]
if !ok {
return nil, fmt.Errorf("unknown resource type: %s", s.Type)
}
return r.Apply(s, d, p.meta)
}
// Diff implementation of terraform.ResourceProvider interface.
func (p *Provider) Diff(
s *terraform.ResourceState,
c *terraform.ResourceConfig) (*terraform.ResourceDiff, error) {
r, ok := p.ResourcesMap[s.Type]
if !ok {
return nil, fmt.Errorf("unknown resource type: %s", s.Type)
}
return r.Diff(s, c)
}
// Refresh implementation of terraform.ResourceProvider interface.
func (p *Provider) Refresh(
s *terraform.ResourceState) (*terraform.ResourceState, error) {
r, ok := p.ResourcesMap[s.Type]
if !ok {
return nil, fmt.Errorf("unknown resource type: %s", s.Type)
}
return r.Refresh(s, p.meta)
}
// Resources implementation of terraform.ResourceProvider interface.
func (p *Provider) Resources() []terraform.ResourceType {
keys := make([]string, 0, len(p.ResourcesMap))
for k, _ := range p.ResourcesMap {
keys = append(keys, k)
}
sort.Strings(keys)
result := make([]terraform.ResourceType, 0, len(keys))
for _, k := range keys {
result = append(result, terraform.ResourceType{
Name: k,
})
}
return result
}

View File

@ -0,0 +1,157 @@
package schema
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
)
func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = new(Provider)
}
func TestProviderConfigure(t *testing.T) {
cases := []struct {
P *Provider
Config map[string]interface{}
Err bool
}{
{
P: &Provider{},
Config: nil,
Err: false,
},
{
P: &Provider{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
ConfigureFunc: func(d *ResourceData) (interface{}, error) {
if d.Get("foo").(int) == 42 {
return nil, nil
}
return nil, fmt.Errorf("nope")
},
},
Config: map[string]interface{}{
"foo": 42,
},
Err: false,
},
{
P: &Provider{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
ConfigureFunc: func(d *ResourceData) (interface{}, error) {
if d.Get("foo").(int) == 42 {
return nil, nil
}
return nil, fmt.Errorf("nope")
},
},
Config: map[string]interface{}{
"foo": 52,
},
Err: true,
},
}
for i, tc := range cases {
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("err: %s", err)
}
err = tc.P.Configure(terraform.NewResourceConfig(c))
if (err != nil) != tc.Err {
t.Fatalf("%d: %s", i, err)
}
}
}
func TestProviderResources(t *testing.T) {
cases := []struct {
P *Provider
Result []terraform.ResourceType
}{
{
P: &Provider{},
Result: []terraform.ResourceType{},
},
{
P: &Provider{
ResourcesMap: map[string]*Resource{
"foo": nil,
"bar": nil,
},
},
Result: []terraform.ResourceType{
terraform.ResourceType{Name: "bar"},
terraform.ResourceType{Name: "foo"},
},
},
}
for i, tc := range cases {
actual := tc.P.Resources()
if !reflect.DeepEqual(actual, tc.Result) {
t.Fatalf("%d: %#v", i, actual)
}
}
}
func TestProviderValidateResource(t *testing.T) {
cases := []struct {
P *Provider
Type string
Config map[string]interface{}
Err bool
}{
{
P: &Provider{},
Type: "foo",
Config: nil,
Err: true,
},
{
P: &Provider{
ResourcesMap: map[string]*Resource{
"foo": &Resource{},
},
},
Type: "foo",
Config: nil,
Err: false,
},
}
for i, tc := range cases {
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("err: %s", err)
}
_, es := tc.P.ValidateResource(tc.Type, terraform.NewResourceConfig(c))
if (len(es) > 0) != tc.Err {
t.Fatalf("%d: %#v", i, es)
}
}
}

135
helper/schema/resource.go Normal file
View File

@ -0,0 +1,135 @@
package schema
import (
"errors"
"fmt"
"github.com/hashicorp/terraform/terraform"
)
// The functions below are the CRUD function types for a Resource.
//
// The second parameter is the meta value sent to the resource when
// different operations are called.
type CreateFunc func(*ResourceData, interface{}) error
type ReadFunc func(*ResourceData, interface{}) error
type UpdateFunc func(*ResourceData, interface{}) error
type DeleteFunc func(*ResourceData, interface{}) error
// Resource represents a thing in Terraform that has a set of configurable
// attributes and generally also has a lifecycle (create, read, update,
// delete).
//
// The Resource schema is an abstraction that allows provider writers to
// worry only about CRUD operations while off-loading validation, diff
// generation, etc. to this higher level library.
type Resource struct {
Schema map[string]*Schema
Create CreateFunc
Read ReadFunc
Update UpdateFunc
Delete DeleteFunc
}
// Apply creates, updates, and/or deletes a resource.
func (r *Resource) Apply(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
data, err := schemaMap(r.Schema).Data(s, d)
if err != nil {
return s, err
}
if s == nil {
// The Terraform API dictates that this should never happen, but
// it doesn't hurt to be safe in this case.
s = new(terraform.ResourceState)
}
if d.Destroy || d.RequiresNew() {
if s.ID != "" {
// Destroy the resource since it is created
if err := r.Delete(data, meta); err != nil {
return data.State(), err
}
// Reset the data to be empty
data, err = schemaMap(r.Schema).Data(nil, d)
if err != nil {
return nil, err
}
}
// If we're only destroying, and not creating, then return
// now since we're done!
if !d.RequiresNew() {
return nil, nil
}
}
err = nil
if s.ID == "" {
// We're creating, it is a new resource.
err = r.Create(data, meta)
} else {
if r.Update == nil {
return s, fmt.Errorf("%s doesn't support update", s.Type)
}
err = r.Update(data, meta)
}
// Always set the ID attribute if it is set. We also always collapse
// the state since even partial states need to be returned.
state := data.State()
if state.ID != "" {
if state.Attributes == nil {
state.Attributes = make(map[string]string)
}
state.Attributes["id"] = state.ID
}
return state, err
}
// Diff returns a diff of this resource and is API compatible with the
// ResourceProvider interface.
func (r *Resource) Diff(
s *terraform.ResourceState,
c *terraform.ResourceConfig) (*terraform.ResourceDiff, error) {
return schemaMap(r.Schema).Diff(s, c)
}
// Validate validates the resource configuration against the schema.
func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return schemaMap(r.Schema).Validate(c)
}
// Refresh refreshes the state of the resource.
func (r *Resource) Refresh(
s *terraform.ResourceState,
meta interface{}) (*terraform.ResourceState, error) {
data, err := schemaMap(r.Schema).Data(s, nil)
if err != nil {
return nil, err
}
err = r.Read(data, meta)
return data.State(), err
}
// InternalValidate should be called to validate the structure
// of the resource.
//
// This should be called in a unit test for any resource to verify
// before release that a resource is properly configured for use with
// this library.
func (r *Resource) InternalValidate() error {
if r == nil {
return errors.New("resource is nil")
}
return schemaMap(r.Schema).InternalValidate()
}

View File

@ -0,0 +1,629 @@
package schema
import (
"fmt"
"reflect"
"strconv"
"strings"
"sync"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/mapstructure"
)
// getSource represents the level we want to get for a value (internally).
// Any source less than or equal to the level will be loaded (whichever
// has a value first).
type getSource byte
const (
getSourceState getSource = iota
getSourceDiff
getSourceSet
)
// ResourceData is used to query and set the attributes of a resource.
type ResourceData struct {
schema map[string]*Schema
state *terraform.ResourceState
diff *terraform.ResourceDiff
setMap map[string]string
newState *terraform.ResourceState
once sync.Once
}
// Get returns the data for the given key, or nil if the key doesn't exist.
//
// The type of the data returned will be according to the schema specified.
// Primitives will be their respective types in Go, lists will always be
// []interface{}, and sub-resources will be map[string]interface{}.
func (d *ResourceData) Get(key string) interface{} {
var parts []string
if key != "" {
parts = strings.Split(key, ".")
}
return d.getObject("", parts, d.schema, getSourceSet)
}
// GetChange returns the old and new value for a given key.
//
// If there is no change, then old and new will simply be the same.
func (d *ResourceData) GetChange(key string) (interface{}, interface{}) {
var parts []string
if key != "" {
parts = strings.Split(key, ".")
}
o := d.getObject("", parts, d.schema, getSourceState)
n := d.getObject("", parts, d.schema, getSourceDiff)
return o, n
}
// HasChange returns whether or not the given key has been changed.
func (d *ResourceData) HasChange(key string) bool {
o, n := d.GetChange(key)
return !reflect.DeepEqual(o, n)
}
// Set sets the value for the given key.
//
// If the key is invalid or the value is not a correct type, an error
// will be returned.
func (d *ResourceData) Set(key string, value interface{}) error {
if d.setMap == nil {
d.setMap = make(map[string]string)
}
parts := strings.Split(key, ".")
return d.setObject("", parts, d.schema, value)
}
// Id returns the ID of the resource.
func (d *ResourceData) Id() string {
var result string
if d.state != nil {
result = d.state.ID
}
if d.newState != nil {
result = d.newState.ID
}
return result
}
// Dependencies returns the dependencies in this state.
func (d *ResourceData) Dependencies() []terraform.ResourceDependency {
if d.newState != nil {
return d.newState.Dependencies
}
if d.state != nil {
return d.state.Dependencies
}
return nil
}
// SetId sets the ID of the resource. If the value is blank, then the
// resource is destroyed.
func (d *ResourceData) SetId(v string) {
d.once.Do(d.init)
d.newState.ID = v
}
// SetDependencies sets the dependencies of a resource.
func (d *ResourceData) SetDependencies(ds []terraform.ResourceDependency) {
d.once.Do(d.init)
d.newState.Dependencies = ds
}
// State returns the new ResourceState after the diff and any Set
// calls.
func (d *ResourceData) State() *terraform.ResourceState {
var result terraform.ResourceState
result.ID = d.Id()
result.Attributes = d.stateObject("", d.schema)
result.Dependencies = d.Dependencies()
return &result
}
func (d *ResourceData) init() {
var copyState terraform.ResourceState
if d.state != nil {
copyState = *d.state
}
d.newState = &copyState
}
func (d *ResourceData) get(
k string,
parts []string,
schema *Schema,
source getSource) interface{} {
switch schema.Type {
case TypeList:
return d.getList(k, parts, schema, source)
case TypeMap:
return d.getMap(k, parts, schema, source)
default:
return d.getPrimitive(k, parts, schema, source)
}
}
func (d *ResourceData) getMap(
k string,
parts []string,
schema *Schema,
source getSource) interface{} {
elemSchema := &Schema{Type: TypeString}
result := make(map[string]interface{})
prefix := k + "."
if d.state != nil && source >= getSourceState {
for k, _ := range d.state.Attributes {
if !strings.HasPrefix(k, prefix) {
continue
}
single := k[len(prefix):]
result[single] = d.getPrimitive(k, nil, elemSchema, source)
}
}
if d.diff != nil && source >= getSourceDiff {
for k, v := range d.diff.Attributes {
if !strings.HasPrefix(k, prefix) {
continue
}
single := k[len(prefix):]
if v.NewRemoved {
delete(result, single)
} else {
result[single] = d.getPrimitive(k, nil, elemSchema, source)
}
}
}
if d.setMap != nil && source >= getSourceSet {
cleared := false
for k, _ := range d.setMap {
if !strings.HasPrefix(k, prefix) {
continue
}
if !cleared {
// We clear the results if they are in the set map
result = make(map[string]interface{})
cleared = true
}
single := k[len(prefix):]
result[single] = d.getPrimitive(k, nil, elemSchema, source)
}
}
return result
}
func (d *ResourceData) getObject(
k string,
parts []string,
schema map[string]*Schema,
source getSource) interface{} {
if len(parts) > 0 {
// We're requesting a specific key in an object
key := parts[0]
parts = parts[1:]
s, ok := schema[key]
if !ok {
return nil
}
if k != "" {
// If we're not at the root, then we need to append
// the key to get the full key path.
key = fmt.Sprintf("%s.%s", k, key)
}
return d.get(key, parts, s, source)
}
// Get the entire object
result := make(map[string]interface{})
for field, _ := range schema {
result[field] = d.getObject(k, []string{field}, schema, source)
}
return result
}
func (d *ResourceData) getList(
k string,
parts []string,
schema *Schema,
source getSource) interface{} {
if len(parts) > 0 {
// We still have parts left over meaning we're accessing an
// element of this list.
idx := parts[0]
parts = parts[1:]
// Special case if we're accessing the count of the list
if idx == "#" {
schema := &Schema{Type: TypeInt}
result := d.get(k+".#", parts, schema, source)
if result == nil {
result = 0
}
return result
}
key := fmt.Sprintf("%s.%s", k, idx)
switch t := schema.Elem.(type) {
case *Resource:
return d.getObject(key, parts, t.Schema, source)
case *Schema:
return d.get(key, parts, t, source)
}
}
// Get the entire list.
result := make(
[]interface{},
d.getList(k, []string{"#"}, schema, source).(int))
for i, _ := range result {
is := strconv.FormatInt(int64(i), 10)
result[i] = d.getList(k, []string{is}, schema, source)
}
return result
}
func (d *ResourceData) getPrimitive(
k string,
parts []string,
schema *Schema,
source getSource) interface{} {
var result string
var resultSet bool
if d.state != nil && source >= getSourceState {
result, resultSet = d.state.Attributes[k]
}
if d.diff != nil && source >= getSourceDiff {
attrD, ok := d.diff.Attributes[k]
if ok && !attrD.NewComputed {
result = attrD.New
resultSet = true
}
}
if d.setMap != nil && source >= getSourceSet {
if v, ok := d.setMap[k]; ok {
result = v
resultSet = true
}
}
if !resultSet {
return nil
}
switch schema.Type {
case TypeString:
// Use the value as-is. We just put this case here to be explicit.
return result
case TypeInt:
if result == "" {
return 0
}
v, err := strconv.ParseInt(result, 0, 0)
if err != nil {
panic(err)
}
return int(v)
default:
panic(fmt.Sprintf("Unknown type: %s", schema.Type))
}
}
func (d *ResourceData) set(
k string,
parts []string,
schema *Schema,
value interface{}) error {
switch schema.Type {
case TypeList:
return d.setList(k, parts, schema, value)
case TypeMap:
return d.setMapValue(k, parts, schema, value)
default:
return d.setPrimitive(k, schema, value)
}
}
func (d *ResourceData) setList(
k string,
parts []string,
schema *Schema,
value interface{}) error {
if len(parts) > 0 {
// We're setting a specific element
idx := parts[0]
parts = parts[1:]
// Special case if we're accessing the count of the list
if idx == "#" {
return fmt.Errorf("%s: can't set count of list", k)
}
key := fmt.Sprintf("%s.%s", k, idx)
switch t := schema.Elem.(type) {
case *Resource:
return d.setObject(key, parts, t.Schema, value)
case *Schema:
return d.set(key, parts, t, value)
}
}
var vs []interface{}
if err := mapstructure.Decode(value, &vs); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
// Set the entire list.
var err error
for i, elem := range vs {
is := strconv.FormatInt(int64(i), 10)
err = d.setList(k, []string{is}, schema, elem)
if err != nil {
break
}
}
if err != nil {
for i, _ := range vs {
is := strconv.FormatInt(int64(i), 10)
d.setList(k, []string{is}, schema, nil)
}
return err
}
d.setMap[k+".#"] = strconv.FormatInt(int64(len(vs)), 10)
return nil
}
func (d *ResourceData) setMapValue(
k string,
parts []string,
schema *Schema,
value interface{}) error {
elemSchema := &Schema{Type: TypeString}
if len(parts) > 0 {
return fmt.Errorf("%s: full map must be set, no a single element", k)
}
// Delete any prior map set
/*
v := d.getMap(k, nil, schema, getSourceSet)
for subKey, _ := range v.(map[string]interface{}) {
delete(d.setMap, fmt.Sprintf("%s.%s", k, subKey))
}
*/
v := reflect.ValueOf(value)
if v.Kind() != reflect.Map {
return fmt.Errorf("%s: must be a map", k)
}
if v.Type().Key().Kind() != reflect.String {
return fmt.Errorf("%s: keys must strings", k)
}
vs := make(map[string]interface{})
for _, mk := range v.MapKeys() {
mv := v.MapIndex(mk)
vs[mk.String()] = mv.Interface()
}
for subKey, v := range vs {
err := d.set(fmt.Sprintf("%s.%s", k, subKey), nil, elemSchema, v)
if err != nil {
return err
}
}
return nil
}
func (d *ResourceData) setObject(
k string,
parts []string,
schema map[string]*Schema,
value interface{}) error {
if len(parts) > 0 {
// We're setting a specific key in an object
key := parts[0]
parts = parts[1:]
s, ok := schema[key]
if !ok {
return fmt.Errorf("%s (internal): unknown key to set: %s", k, key)
}
if k != "" {
// If we're not at the root, then we need to append
// the key to get the full key path.
key = fmt.Sprintf("%s.%s", k, key)
}
return d.set(key, parts, s, value)
}
// Set the entire object. First decode into a proper structure
var v map[string]interface{}
if err := mapstructure.Decode(value, &v); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
// Set each element in turn
var err error
for k1, v1 := range v {
err = d.setObject(k, []string{k1}, schema, v1)
if err != nil {
break
}
}
if err != nil {
for k1, _ := range v {
d.setObject(k, []string{k1}, schema, nil)
}
}
return err
}
func (d *ResourceData) setPrimitive(
k string,
schema *Schema,
v interface{}) error {
if v == nil {
delete(d.setMap, k)
return nil
}
var set string
switch schema.Type {
case TypeString:
if err := mapstructure.Decode(v, &set); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
case TypeInt:
var n int
if err := mapstructure.Decode(v, &n); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
set = strconv.FormatInt(int64(n), 10)
default:
return fmt.Errorf("Unknown type: %s", schema.Type)
}
d.setMap[k] = set
return nil
}
func (d *ResourceData) stateList(
prefix string,
schema *Schema) map[string]string {
countRaw := d.get(prefix, []string{"#"}, schema, getSourceSet)
if countRaw == nil {
return nil
}
count := countRaw.(int)
result := make(map[string]string)
if count > 0 {
result[prefix+".#"] = strconv.FormatInt(int64(count), 10)
}
for i := 0; i < count; i++ {
key := fmt.Sprintf("%s.%d", prefix, i)
var m map[string]string
switch t := schema.Elem.(type) {
case *Resource:
m = d.stateObject(key, t.Schema)
case *Schema:
m = d.stateSingle(key, t)
}
for k, v := range m {
result[k] = v
}
}
return result
}
func (d *ResourceData) stateMap(
prefix string,
schema *Schema) map[string]string {
v := d.getMap(prefix, nil, schema, getSourceSet)
if v == nil {
return nil
}
elemSchema := &Schema{Type: TypeString}
result := make(map[string]string)
for mk, _ := range v.(map[string]interface{}) {
mp := fmt.Sprintf("%s.%s", prefix, mk)
for k, v := range d.stateSingle(mp, elemSchema) {
result[k] = v
}
}
return result
}
func (d *ResourceData) stateObject(
prefix string,
schema map[string]*Schema) map[string]string {
result := make(map[string]string)
for k, v := range schema {
key := k
if prefix != "" {
key = prefix + "." + key
}
for k1, v1 := range d.stateSingle(key, v) {
result[k1] = v1
}
}
return result
}
func (d *ResourceData) statePrimitive(
prefix string,
schema *Schema) map[string]string {
v := d.getPrimitive(prefix, nil, schema, getSourceSet)
if v == nil {
return nil
}
var vs string
switch schema.Type {
case TypeString:
vs = v.(string)
case TypeInt:
vs = strconv.FormatInt(int64(v.(int)), 10)
default:
panic(fmt.Sprintf("Unknown type: %s", schema.Type))
}
return map[string]string{
prefix: vs,
}
}
func (d *ResourceData) stateSingle(
prefix string,
schema *Schema) map[string]string {
switch schema.Type {
case TypeList:
return d.stateList(prefix, schema)
case TypeMap:
return d.stateMap(prefix, schema)
default:
return d.statePrimitive(prefix, schema)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,306 @@
package schema
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestResourceApply_create(t *testing.T) {
r := &Resource{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
}
called := false
r.Create = func(d *ResourceData, m interface{}) error {
called = true
d.SetId("foo")
return nil
}
var s *terraform.ResourceState = nil
d := &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
New: "42",
},
},
}
actual, err := r.Apply(s, d, nil)
if err != nil {
t.Fatalf("err: %s", err)
}
if !called {
t.Fatal("not called")
}
expected := &terraform.ResourceState{
ID: "foo",
Attributes: map[string]string{
"id": "foo",
"foo": "42",
},
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceApply_destroy(t *testing.T) {
r := &Resource{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
}
called := false
r.Delete = func(d *ResourceData, m interface{}) error {
called = true
return nil
}
s := &terraform.ResourceState{
ID: "bar",
}
d := &terraform.ResourceDiff{
Destroy: true,
}
actual, err := r.Apply(s, d, nil)
if err != nil {
t.Fatalf("err: %s", err)
}
if !called {
t.Fatal("delete not called")
}
if actual != nil {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceApply_destroyPartial(t *testing.T) {
r := &Resource{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
}
r.Delete = func(d *ResourceData, m interface{}) error {
d.Set("foo", 42)
return fmt.Errorf("some error")
}
s := &terraform.ResourceState{
ID: "bar",
Attributes: map[string]string{
"foo": "12",
},
}
d := &terraform.ResourceDiff{
Destroy: true,
}
actual, err := r.Apply(s, d, nil)
if err == nil {
t.Fatal("should error")
}
expected := &terraform.ResourceState{
ID: "bar",
Attributes: map[string]string{
"foo": "42",
},
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceApply_update(t *testing.T) {
r := &Resource{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
}
r.Update = func(d *ResourceData, m interface{}) error {
d.Set("foo", 42)
return nil
}
s := &terraform.ResourceState{
ID: "foo",
Attributes: map[string]string{
"foo": "12",
},
}
d := &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
New: "13",
},
},
}
actual, err := r.Apply(s, d, nil)
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &terraform.ResourceState{
ID: "foo",
Attributes: map[string]string{
"id": "foo",
"foo": "42",
},
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceApply_updateNoCallback(t *testing.T) {
r := &Resource{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
}
r.Update = nil
s := &terraform.ResourceState{
ID: "foo",
Attributes: map[string]string{
"foo": "12",
},
}
d := &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
New: "13",
},
},
}
actual, err := r.Apply(s, d, nil)
if err == nil {
t.Fatal("should error")
}
expected := &terraform.ResourceState{
ID: "foo",
Attributes: map[string]string{
"foo": "12",
},
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceInternalValidate(t *testing.T) {
cases := []struct {
In *Resource
Err bool
}{
{
nil,
true,
},
// No optional and no required
{
&Resource{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
Required: true,
},
},
},
true,
},
}
for i, tc := range cases {
err := tc.In.InternalValidate()
if (err != nil) != tc.Err {
t.Fatalf("%d: bad: %s", i, err)
}
}
}
func TestResourceRefresh(t *testing.T) {
r := &Resource{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
}
r.Read = func(d *ResourceData, m interface{}) error {
if m != 42 {
return fmt.Errorf("meta not passed")
}
return d.Set("foo", d.Get("foo").(int)+1)
}
s := &terraform.ResourceState{
ID: "bar",
Attributes: map[string]string{
"foo": "12",
},
}
expected := &terraform.ResourceState{
ID: "bar",
Attributes: map[string]string{
"foo": "13",
},
}
actual, err := r.Refresh(s, 42)
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}

564
helper/schema/schema.go Normal file
View File

@ -0,0 +1,564 @@
package schema
import (
"fmt"
"reflect"
"strconv"
"strings"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/mapstructure"
)
// ValueType is an enum of the type that can be represented by a schema.
type ValueType int
const (
TypeInvalid ValueType = iota
TypeBool
TypeInt
TypeString
TypeList
TypeMap
)
// Schema is used to describe the structure of a value.
type Schema struct {
// Type is the type of the value and must be one of the ValueType values.
Type ValueType
// If one of these is set, then this item can come from the configuration.
// Both cannot be set. If Optional is set, the value is optional. If
// Required is set, the value is required.
Optional bool
Required bool
// The fields below relate to diffs.
//
// If Computed is true, then the result of this value is computed
// (unless specified by config) on creation.
//
// If ForceNew is true, then a change in this resource necessitates
// the creation of a new resource.
Computed bool
ForceNew bool
// The following fields are only set for a TypeList Type.
//
// Elem must be either a *Schema or a *Resource only if the Type is
// TypeList, and represents what the element type is. If it is *Schema,
// the element type is just a simple value. If it is *Resource, the
// element type is a complex structure, potentially with its own lifecycle.
Elem interface{}
// ComputedWhen is a set of queries on the configuration. Whenever any
// of these things is changed, it will require a recompute (this requires
// that Computed is set to true).
ComputedWhen []string
}
func (s *Schema) finalizeDiff(
d *terraform.ResourceAttrDiff) *terraform.ResourceAttrDiff {
if d == nil {
return d
}
if s.Computed {
if d.Old != "" && d.New == "" {
// This is a computed value with an old value set already,
// just let it go.
return nil
}
if d.New == "" {
// Computed attribute without a new value set
d.NewComputed = true
}
}
if s.ForceNew {
// Force new, set it to true in the diff
d.RequiresNew = true
}
return d
}
// schemaMap is a wrapper that adds nice functions on top of schemas.
type schemaMap map[string]*Schema
// Data returns a ResourceData for the given schema, state, and diff.
//
// The diff is optional.
func (m schemaMap) Data(
s *terraform.ResourceState,
d *terraform.ResourceDiff) (*ResourceData, error) {
return &ResourceData{
schema: m,
state: s,
diff: d,
}, nil
}
// Diff returns the diff for a resource given the schema map,
// state, and configuration.
func (m schemaMap) Diff(
s *terraform.ResourceState,
c *terraform.ResourceConfig) (*terraform.ResourceDiff, error) {
result := new(terraform.ResourceDiff)
result.Attributes = make(map[string]*terraform.ResourceAttrDiff)
for k, schema := range m {
err := m.diff(k, schema, result, s, c)
if err != nil {
return nil, err
}
}
// Remove any nil diffs just to keep things clean
for k, v := range result.Attributes {
if v == nil {
delete(result.Attributes, k)
}
}
// Go through and detect all of the ComputedWhens now that we've
// finished the diff.
// TODO
if result.Empty() {
// If we don't have any diff elements, just return nil
return nil, nil
}
return result, nil
}
// Validate validates the configuration against this schema mapping.
func (m schemaMap) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return m.validateObject("", m, c)
}
// InternalValidate validates the format of this schema. This should be called
// from a unit test (and not in user-path code) to verify that a schema
// is properly built.
func (m schemaMap) InternalValidate() error {
for k, v := range m {
if v.Type == TypeInvalid {
return fmt.Errorf("%s: Type must be specified", k)
}
if v.Optional && v.Required {
return fmt.Errorf("%s: Optional or Required must be set, not both", k)
}
if v.Required && v.Computed {
return fmt.Errorf("%s: Cannot be both Required and Computed", k)
}
if len(v.ComputedWhen) > 0 && !v.Computed {
return fmt.Errorf("%s: ComputedWhen can only be set with Computed", k)
}
if v.Type == TypeList {
if v.Elem == nil {
return fmt.Errorf("%s: Elem must be set for lists", k)
}
switch t := v.Elem.(type) {
case *Resource:
if err := t.InternalValidate(); err != nil {
return err
}
case *Schema:
bad := t.Computed || t.Optional || t.Required
if bad {
return fmt.Errorf(
"%s: Elem must have only Type set", k)
}
}
}
}
return nil
}
func (m schemaMap) diff(
k string,
schema *Schema,
diff *terraform.ResourceDiff,
s *terraform.ResourceState,
c *terraform.ResourceConfig) error {
var err error
switch schema.Type {
case TypeBool:
fallthrough
case TypeInt:
fallthrough
case TypeString:
err = m.diffString(k, schema, diff, s, c)
case TypeList:
err = m.diffList(k, schema, diff, s, c)
case TypeMap:
err = m.diffMap(k, schema, diff, s, c)
default:
err = fmt.Errorf("%s: unknown type %s", k, schema.Type)
}
return err
}
func (m schemaMap) diffList(
k string,
schema *Schema,
diff *terraform.ResourceDiff,
s *terraform.ResourceState,
c *terraform.ResourceConfig) error {
var vs []interface{}
v, ok := c.Get(k)
if ok {
// We have to use reflection to build the []interface{} list
rawV := reflect.ValueOf(v)
if rawV.Kind() != reflect.Slice {
return fmt.Errorf("%s: must be a list", k)
}
vs = make([]interface{}, rawV.Len())
for i, _ := range vs {
vs[i] = rawV.Index(i).Interface()
}
}
// If this field is required, then it must also be non-empty
if len(vs) == 0 && schema.Required {
return fmt.Errorf("%s: required field is not set", k)
}
// Get the counts
var oldLen, newLen int
if s != nil {
if v, ok := s.Attributes[k+".#"]; ok {
old64, err := strconv.ParseInt(v, 0, 0)
if err != nil {
return err
}
oldLen = int(old64)
}
}
newLen = len(vs)
// If the counts are not the same, then record that diff
changed := oldLen != newLen
computed := oldLen == 0 && newLen == 0 && schema.Computed
if changed || computed {
countSchema := &Schema{
Type: TypeInt,
Computed: schema.Computed,
ForceNew: schema.ForceNew,
}
oldStr := ""
newStr := ""
if !computed {
oldStr = strconv.FormatInt(int64(oldLen), 10)
newStr = strconv.FormatInt(int64(newLen), 10)
}
diff.Attributes[k+".#"] = countSchema.finalizeDiff(&terraform.ResourceAttrDiff{
Old: oldStr,
New: newStr,
})
}
// Figure out the maximum
maxLen := oldLen
if newLen > maxLen {
maxLen = newLen
}
switch t := schema.Elem.(type) {
case *Schema:
// Copy the schema so that we can set Computed/ForceNew from
// the parent schema (the TypeList).
t2 := *t
t2.ForceNew = schema.ForceNew
// This is just a primitive element, so go through each and
// just diff each.
for i := 0; i < maxLen; i++ {
subK := fmt.Sprintf("%s.%d", k, i)
err := m.diff(subK, &t2, diff, s, c)
if err != nil {
return err
}
}
case *Resource:
// This is a complex resource
for i := 0; i < maxLen; i++ {
for k2, schema := range t.Schema {
subK := fmt.Sprintf("%s.%d.%s", k, i, k2)
err := m.diff(subK, schema, diff, s, c)
if err != nil {
return err
}
}
}
default:
return fmt.Errorf("%s: unknown element type (internal)", k)
}
return nil
}
func (m schemaMap) diffMap(
k string,
schema *Schema,
diff *terraform.ResourceDiff,
s *terraform.ResourceState,
c *terraform.ResourceConfig) error {
//elemSchema := &Schema{Type: TypeString}
prefix := k + "."
// First get all the values from the state
stateMap := make(map[string]string)
if s != nil {
for sk, sv := range s.Attributes {
if !strings.HasPrefix(sk, prefix) {
continue
}
stateMap[sk[len(prefix):]] = sv
}
}
// Then get all the values from the configuration
configMap := make(map[string]string)
if c != nil {
if raw, ok := c.Get(k); ok {
for k, v := range raw.(map[string]interface{}) {
configMap[k] = v.(string)
}
}
}
// Now we compare, preferring values from the config map
for k, v := range configMap {
old := stateMap[k]
delete(stateMap, k)
if old == v {
continue
}
diff.Attributes[prefix+k] = schema.finalizeDiff(&terraform.ResourceAttrDiff{
Old: old,
New: v,
})
}
for k, v := range stateMap {
diff.Attributes[prefix+k] = schema.finalizeDiff(&terraform.ResourceAttrDiff{
Old: v,
NewRemoved: true,
})
}
return nil
}
func (m schemaMap) diffString(
k string,
schema *Schema,
diff *terraform.ResourceDiff,
s *terraform.ResourceState,
c *terraform.ResourceConfig) error {
var old, n string
if s != nil {
old = s.Attributes[k]
}
v, ok := c.Get(k)
if !ok {
// We don't have a value, if it is required then it is an error
if schema.Required {
return fmt.Errorf("%s: required field not set", k)
}
// If we don't have an old value, just return
if old == "" && !schema.Computed {
return nil
}
} else {
if err := mapstructure.WeakDecode(v, &n); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
if old == n {
// They're the same value
return nil
}
}
diff.Attributes[k] = schema.finalizeDiff(&terraform.ResourceAttrDiff{
Old: old,
New: n,
})
return nil
}
func (m schemaMap) diffPrimitive(
k string,
nraw interface{},
schema *Schema,
diff *terraform.ResourceDiff,
s *terraform.ResourceState) error {
var old, n string
if s != nil {
old = s.Attributes[k]
}
if err := mapstructure.WeakDecode(nraw, &n); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
if old == n {
// They're the same value
return nil
}
diff.Attributes[k] = schema.finalizeDiff(&terraform.ResourceAttrDiff{
Old: old,
New: n,
})
return nil
}
func (m schemaMap) validate(
k string,
schema *Schema,
c *terraform.ResourceConfig) ([]string, []error) {
raw, ok := c.Get(k)
if !ok {
if schema.Required {
return nil, []error{fmt.Errorf(
"%s: required field is not set", k)}
}
return nil, nil
}
if !schema.Required && !schema.Optional {
// This is a computed-only field
return nil, []error{fmt.Errorf(
"%s: this field cannot be set", k)}
}
return m.validatePrimitive(k, raw, schema, c)
}
func (m schemaMap) validateList(
k string,
raw interface{},
schema *Schema,
c *terraform.ResourceConfig) ([]string, []error) {
// We use reflection to verify the slice because you can't
// case to []interface{} unless the slice is exactly that type.
rawV := reflect.ValueOf(raw)
if rawV.Kind() != reflect.Slice {
return nil, []error{fmt.Errorf(
"%s: should be a list", k)}
}
// Now build the []interface{}
raws := make([]interface{}, rawV.Len())
for i, _ := range raws {
raws[i] = rawV.Index(i).Interface()
}
var ws []string
var es []error
for i, raw := range raws {
key := fmt.Sprintf("%s.%d", k, i)
var ws2 []string
var es2 []error
switch t := schema.Elem.(type) {
case *Resource:
// This is a sub-resource
ws2, es2 = m.validateObject(key, t.Schema, c)
case *Schema:
// This is some sort of primitive
ws2, es2 = m.validatePrimitive(key, raw, t, c)
}
if len(ws2) > 0 {
ws = append(ws, ws2...)
}
if len(es2) > 0 {
es = append(es, es2...)
}
}
return ws, es
}
func (m schemaMap) validateObject(
k string,
schema map[string]*Schema,
c *terraform.ResourceConfig) ([]string, []error) {
var ws []string
var es []error
for subK, s := range schema {
key := subK
if k != "" {
key = fmt.Sprintf("%s.%s", k, subK)
}
ws2, es2 := m.validate(key, s, c)
if len(ws2) > 0 {
ws = append(ws, ws2...)
}
if len(es2) > 0 {
es = append(es, es2...)
}
}
// Detect any extra/unknown keys and report those as errors.
prefix := k + "."
for configK, _ := range c.Raw {
if k != "" {
if !strings.HasPrefix(configK, prefix) {
continue
}
configK = configK[len(prefix):]
}
if _, ok := schema[configK]; !ok {
es = append(es, fmt.Errorf(
"%s: invalid or unknown key: %s", k, configK))
}
}
return ws, es
}
func (m schemaMap) validatePrimitive(
k string,
raw interface{},
schema *Schema,
c *terraform.ResourceConfig) ([]string, []error) {
switch schema.Type {
case TypeList:
return m.validateList(k, raw, schema, c)
case TypeInt:
// Verify that we can parse this as an int
var n int
if err := mapstructure.WeakDecode(raw, &n); err != nil {
return nil, []error{err}
}
}
return nil, nil
}

1019
helper/schema/schema_test.go Normal file

File diff suppressed because it is too large Load Diff

52
main.go
View File

@ -107,6 +107,28 @@ func wrappedMain() int {
HelpWriter: os.Stdout,
}
// Load the configuration file if we have one, that can be used to
// define extra providers and provisioners.
clicfgFile, err := cliConfigFile()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading CLI configuration: \n\n%s\n", err)
return 1
}
if clicfgFile != "" {
usrcfg, err := LoadConfig(clicfgFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading CLI configuration: \n\n%s\n", err)
return 1
}
config = *config.Merge(usrcfg)
}
// Initialize the TFConfig settings for the commands...
ContextOpts.Providers = config.ProviderFactories()
ContextOpts.Provisioners = config.ProvisionerFactories()
exitCode, err := cli.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error())
@ -116,6 +138,36 @@ func wrappedMain() int {
return exitCode
}
func cliConfigFile() (string, error) {
mustExist := true
configFilePath := os.Getenv("TERRAFORM_CONFIG")
if configFilePath == "" {
var err error
configFilePath, err = configFile()
mustExist = false
if err != nil {
log.Printf(
"[ERROR] Error detecting default CLI config file path: %s",
err)
}
}
log.Printf("[DEBUG] Attempting to open CLI config file: %s", configFilePath)
f, err := os.Open(configFilePath)
if err == nil {
f.Close()
return configFilePath, nil
}
if mustExist || !os.IsNotExist(err) {
return "", err
}
log.Println("[DEBUG] File doesn't exist, but doesn't need to. Ignoring.")
return "", nil
}
// copyOutput uses output prefixes to determine whether data on stdout
// should go to stdout or stderr. This is due to panicwrap using stderr
// as the log and error channel.

View File

@ -45,11 +45,6 @@ gox \
mv bin/terraform-terraform${EXTENSION} bin/terraform${EXTENSION}
cp bin/terraform* ${GOPATHSINGLE}/bin
# If we're on Windows, get the DLL in there
if [ "$(go env GOOS)" = "windows" ]; then
cp libucl.dll bin/
fi
if [ "${TF_DEV}x" = "x" ]; then
# Zip and copy to the dist dir
echo "--> Packaging..."

View File

@ -75,9 +75,11 @@ func NewContext(opts *ContextOpts) *Context {
// Calculate all the default variables
defaultVars := make(map[string]string)
for _, v := range opts.Config.Variables {
for k, val := range v.DefaultsMap() {
defaultVars[k] = val
if opts.Config != nil {
for _, v := range opts.Config.Variables {
for k, val := range v.DefaultsMap() {
defaultVars[k] = val
}
}
}

View File

@ -376,6 +376,11 @@ func graphAddDiff(g *depgraph.Graph, d *Diff) error {
// dependencies. Look to see if they're managed.
for _, dep := range deps {
for _, n2 := range nlist {
// Don't ever depend on ourselves
if n2.Name == n.Name {
continue
}
rn2 := n2.Meta.(*GraphNodeResource)
if rn2.Resource.State.ID == dep.ID {
n2.Deps = append(n2.Deps, &depgraph.Dependency{

View File

@ -14,6 +14,7 @@ import (
func init() {
gob.Register(make([]interface{}, 0))
gob.Register(make([]map[string]interface{}, 0))
gob.Register(make(map[string]interface{}))
gob.Register(make(map[string]string))
}

View File

@ -53,7 +53,7 @@ func TestReadWritePlan(t *testing.T) {
t.Fatalf("err: %s", err)
}
println(reflect.DeepEqual(actual.Config.Variables, plan.Config.Variables))
println(reflect.DeepEqual(actual.Config.Resources, plan.Config.Resources))
if !reflect.DeepEqual(actual, plan) {
t.Fatalf("bad: %#v", actual)

View File

@ -89,34 +89,14 @@ func (c *ResourceConfig) CheckSet(keys []string) []error {
// The second return value is true if the get was successful. Get will
// not succeed if the value is being computed.
func (c *ResourceConfig) Get(k string) (interface{}, bool) {
parts := strings.Split(k, ".")
var current interface{} = c.Raw
for _, part := range parts {
if current == nil {
return nil, false
}
cv := reflect.ValueOf(current)
switch cv.Kind() {
case reflect.Map:
v := cv.MapIndex(reflect.ValueOf(part))
if !v.IsValid() {
return nil, false
}
current = v.Interface()
case reflect.Slice:
i, err := strconv.ParseInt(part, 0, 0)
if err != nil {
return nil, false
}
current = cv.Index(int(i)).Interface()
default:
panic(fmt.Sprintf("Unknown kind: %s", cv.Kind()))
}
// First try to get it from c.Config since that has interpolated values
result, ok := c.get(k, c.Config)
if ok {
return result, ok
}
return current, true
// Otherwise, just get it from the raw config
return c.get(k, c.Raw)
}
// IsSet checks if the key in the configuration is set. A key is set if
@ -143,6 +123,42 @@ func (c *ResourceConfig) IsSet(k string) bool {
return false
}
func (c *ResourceConfig) get(
k string, raw map[string]interface{}) (interface{}, bool) {
parts := strings.Split(k, ".")
var current interface{} = raw
for _, part := range parts {
if current == nil {
return nil, false
}
cv := reflect.ValueOf(current)
switch cv.Kind() {
case reflect.Map:
v := cv.MapIndex(reflect.ValueOf(part))
if !v.IsValid() {
return nil, false
}
current = v.Interface()
case reflect.Slice:
if part == "#" {
current = cv.Len()
} else {
i, err := strconv.ParseInt(part, 0, 0)
if err != nil {
return nil, false
}
current = cv.Index(int(i)).Interface()
}
default:
panic(fmt.Sprintf("Unknown kind: %s", cv.Kind()))
}
}
return current, true
}
func (c *ResourceConfig) interpolate(ctx *Context) error {
if c == nil {
return nil

View File

@ -3,6 +3,8 @@ package terraform
import (
"reflect"
"testing"
"github.com/hashicorp/terraform/config"
)
func TestResource_Vars(t *testing.T) {
@ -29,3 +31,49 @@ func TestResource_Vars(t *testing.T) {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceConfigGet(t *testing.T) {
cases := []struct {
Config map[string]interface{}
Vars map[string]string
Key string
Value interface{}
}{
{
Config: map[string]interface{}{
"foo": "${var.foo}",
},
Key: "foo",
Value: "${var.foo}",
},
{
Config: map[string]interface{}{
"foo": "${var.foo}",
},
Vars: map[string]string{"foo": "bar"},
Key: "foo",
Value: "bar",
},
}
for i, tc := range cases {
rawC, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("err: %s", err)
}
rc := NewResourceConfig(rawC)
if tc.Vars != nil {
ctx := NewContext(&ContextOpts{Variables: tc.Vars})
if err := rc.interpolate(ctx); err != nil {
t.Fatalf("err: %s", err)
}
}
v, _ := rc.Get(tc.Key)
if !reflect.DeepEqual(v, tc.Value) {
t.Fatalf("%d bad: %#v", i, v)
}
}
}

View File

@ -1,7 +1,7 @@
variable "amis" {
default = {
"us-east-1": "foo",
"us-west-2": "foo",
us-east-1 = "foo"
us-west-2 = "foo"
}
}

View File

@ -1,6 +1,6 @@
variable "foo" {
default = "bar";
description = "bar";
default = "bar"
description = "bar"
}
provider "aws" {

View File

@ -1,6 +1,6 @@
variable "foo" {
default = "bar";
description = "bar";
default = "bar"
description = "bar"
}
provider "aws" {

View File

@ -1,6 +1,6 @@
variable "foo" {
default = "bar";
description = "bar";
default = "bar"
description = "bar"
}
provider "aws" {}

View File

@ -10,6 +10,6 @@ variable "bar" {
# Mapping
variable "map" {
default = {
"foo" = "bar";
foo = "bar"
}
}

View File

@ -1,4 +1,4 @@
providers {
"aws" = "foo"
"do" = "bar"
aws = "foo"
do = "bar"
}

View File

@ -33,8 +33,8 @@ end
module DownloadHelpers
def download_arch(file)
parts = file.split("_")
return "" if parts.length != 4
parts[3].split(".")[0]
return "" if parts.length != 3
parts[2].split(".")[0]
end
def download_os_human(os)

View File

@ -41,6 +41,8 @@ The following arguments are supported:
* `name` - (Required) The name of the ELB
* `availability_zones` - (Optional) The AZ's to serve traffic in.
* `security_groups` - (Optional) A list of security group IDs to assign to the ELB.
* `subnets` - (Optional) A list of subnets to attach to the ELB.
* `instances` - (Optional) A list of instance ids to place in the ELB pool.
* `listener` - (Required) A list of listener blocks. Listeners documented below.
* `health_check` - (Required) A health_check block. Health Check documented below.

View File

@ -72,9 +72,11 @@ access_key = "foo"
secret_key = "bar"
```
If a "terraform.tfvars" file is present, Terraform automatically loads
it to populate variables. If the file is named something else, you can
use the `-var-file` flag directly to specify a file.
If a "terraform.tfvars" file is present in the current directory,
Terraform automatically loads it to populate variables. If the file is
named something else, you can use the `-var-file` flag directly to
specify a file. Like configuration files, variable files can also be
JSON.
We recommend using the "terraform.tfvars" file, and ignoring it from
version control.