mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Merge branch 'master' into expand-user-path
This commit is contained in:
commit
f48b888477
23
CHANGELOG.md
23
CHANGELOG.md
@ -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)
|
||||
|
||||
|
49
Makefile
49
Makefile
@ -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
|
||||
|
@ -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
|
||||
|
2
TODO.md
2
TODO.md
@ -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)
|
||||
|
@ -6,5 +6,5 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin.Serve(new(heroku.ResourceProvider))
|
||||
plugin.Serve(heroku.Provider())
|
||||
}
|
||||
|
@ -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}"]
|
||||
}
|
||||
`
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
45
builtin/providers/heroku/provider.go
Normal file
45
builtin/providers/heroku/provider.go
Normal 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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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.*",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
}
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
44
config.go
44
config.go
@ -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
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -36,7 +36,7 @@ func loadTree(root string) (*importTree, error) {
|
||||
case ".tf":
|
||||
fallthrough
|
||||
case ".tf.json":
|
||||
f = loadFileLibucl
|
||||
f = loadFileHcl
|
||||
default:
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
*/
|
9
config/loader_hcl_test.go
Normal file
9
config/loader_hcl_test.go
Normal file
@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHCLConfigurableConfigurable(t *testing.T) {
|
||||
var _ configurable = new(hclConfigurable)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
|
@ -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" {
|
||||
|
@ -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" {
|
||||
|
@ -1,5 +1,5 @@
|
||||
provider "do" {
|
||||
api_key = "${var.foo}";
|
||||
api_key = "${var.foo}"
|
||||
}
|
||||
|
||||
resource "aws_security_group" "firewall" {
|
||||
|
@ -1,6 +1,6 @@
|
||||
variable "foo" {
|
||||
default = "bar";
|
||||
description = "bar";
|
||||
default = "bar"
|
||||
description = "bar"
|
||||
}
|
||||
|
||||
resource "aws_instance" "db" {
|
||||
|
@ -2,7 +2,7 @@
|
||||
"resource": {
|
||||
"aws_instance": {
|
||||
"web": {
|
||||
"foo": "bar",
|
||||
"foo": "bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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" {
|
||||
|
@ -1,5 +1,5 @@
|
||||
provider "do" {
|
||||
api_key = "${var.foo}";
|
||||
api_key = "${var.foo}"
|
||||
}
|
||||
|
||||
resource "aws_security_group" "firewall" {
|
||||
|
@ -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" {}
|
||||
|
@ -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" {
|
||||
|
@ -1 +1 @@
|
||||
what "is this"
|
||||
what "is this" {}
|
||||
|
@ -1,8 +1,8 @@
|
||||
variable "foo" {
|
||||
default = "bar";
|
||||
description = "bar";
|
||||
default = "bar"
|
||||
description = "bar"
|
||||
}
|
||||
|
||||
provider "do" {
|
||||
api_key = "${var.bar}";
|
||||
api_key = "${var.bar}"
|
||||
}
|
||||
|
@ -4,6 +4,6 @@ variable "foo" {
|
||||
|
||||
variable "foo" {
|
||||
default = {
|
||||
"foo" = "bar"
|
||||
foo = "bar"
|
||||
}
|
||||
}
|
||||
|
45
config_unix.go
Normal file
45
config_unix.go
Normal 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
37
config_windows.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
11
helper/schema/README.md
Normal 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
160
helper/schema/provider.go
Normal 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
|
||||
}
|
157
helper/schema/provider_test.go
Normal file
157
helper/schema/provider_test.go
Normal 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
135
helper/schema/resource.go
Normal 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()
|
||||
}
|
629
helper/schema/resource_data.go
Normal file
629
helper/schema/resource_data.go
Normal 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 = ©State
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
1319
helper/schema/resource_data_test.go
Normal file
1319
helper/schema/resource_data_test.go
Normal file
File diff suppressed because it is too large
Load Diff
306
helper/schema/resource_test.go
Normal file
306
helper/schema/resource_test.go
Normal 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
564
helper/schema/schema.go
Normal 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
1019
helper/schema/schema_test.go
Normal file
File diff suppressed because it is too large
Load Diff
52
main.go
52
main.go
@ -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.
|
||||
|
@ -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..."
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
variable "amis" {
|
||||
default = {
|
||||
"us-east-1": "foo",
|
||||
"us-west-2": "foo",
|
||||
us-east-1 = "foo"
|
||||
us-west-2 = "foo"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
variable "foo" {
|
||||
default = "bar";
|
||||
description = "bar";
|
||||
default = "bar"
|
||||
description = "bar"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
|
@ -1,6 +1,6 @@
|
||||
variable "foo" {
|
||||
default = "bar";
|
||||
description = "bar";
|
||||
default = "bar"
|
||||
description = "bar"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
|
@ -1,6 +1,6 @@
|
||||
variable "foo" {
|
||||
default = "bar";
|
||||
description = "bar";
|
||||
default = "bar"
|
||||
description = "bar"
|
||||
}
|
||||
|
||||
provider "aws" {}
|
||||
|
@ -10,6 +10,6 @@ variable "bar" {
|
||||
# Mapping
|
||||
variable "map" {
|
||||
default = {
|
||||
"foo" = "bar";
|
||||
foo = "bar"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
providers {
|
||||
"aws" = "foo"
|
||||
"do" = "bar"
|
||||
aws = "foo"
|
||||
do = "bar"
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user