Merge pull request #201 from hashicorp/f-resource
High-Level Framework for Writing Providers and Resources
This commit is contained in:
commit
839f9d84c8
|
@ -6,5 +6,5 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
plugin.Serve(new(heroku.ResourceProvider))
|
plugin.Serve(new(heroku.Provider()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bgentry/heroku-go"
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testAccProviders map[string]terraform.ResourceProvider
|
var testAccProviders map[string]terraform.ResourceProvider
|
||||||
var testAccProvider *ResourceProvider
|
var testAccProvider *schema.Provider
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
testAccProvider = new(ResourceProvider)
|
testAccProvider = Provider()
|
||||||
testAccProviders = map[string]terraform.ResourceProvider{
|
testAccProviders = map[string]terraform.ResourceProvider{
|
||||||
"heroku": testAccProvider,
|
"heroku": testAccProvider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResourceProvider_impl(t *testing.T) {
|
func TestProvider(t *testing.T) {
|
||||||
var _ terraform.ResourceProvider = new(ResourceProvider)
|
if err := Provider().InternalValidate(); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResourceProvider_Configure(t *testing.T) {
|
func TestProvider_impl(t *testing.T) {
|
||||||
rp := new(ResourceProvider)
|
var _ terraform.ResourceProvider = Provider()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderConfigure(t *testing.T) {
|
||||||
var expectedKey string
|
var expectedKey string
|
||||||
var expectedEmail string
|
var expectedEmail string
|
||||||
|
|
||||||
|
@ -50,18 +56,18 @@ func TestResourceProvider_Configure(t *testing.T) {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rp := Provider()
|
||||||
err = rp.Configure(terraform.NewResourceConfig(rawConfig))
|
err = rp.Configure(terraform.NewResourceConfig(rawConfig))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := Config{
|
config := rp.Meta().(*heroku.Client)
|
||||||
APIKey: expectedKey,
|
if config.Username != expectedEmail {
|
||||||
Email: expectedEmail,
|
t.Fatalf("bad: %#v", config)
|
||||||
}
|
}
|
||||||
|
if config.Password != expectedKey {
|
||||||
if !reflect.DeepEqual(rp.Config, expected) {
|
t.Fatalf("bad: %#v", config)
|
||||||
t.Fatalf("bad: %#v", rp.Config)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/bgentry/heroku-go"
|
"github.com/bgentry/heroku-go"
|
||||||
"github.com/hashicorp/terraform/flatmap"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
"github.com/hashicorp/terraform/helper/config"
|
|
||||||
"github.com/hashicorp/terraform/helper/diff"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,163 +15,133 @@ import (
|
||||||
// multiple addons simultaneously.
|
// multiple addons simultaneously.
|
||||||
var addonLock sync.Mutex
|
var addonLock sync.Mutex
|
||||||
|
|
||||||
func resource_heroku_addon_create(
|
func resourceHerokuAddon() *schema.Resource {
|
||||||
s *terraform.ResourceState,
|
return &schema.Resource{
|
||||||
d *terraform.ResourceDiff,
|
Create: resourceHerokuAddonCreate,
|
||||||
meta interface{}) (*terraform.ResourceState, error) {
|
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()
|
addonLock.Lock()
|
||||||
defer addonLock.Unlock()
|
defer addonLock.Unlock()
|
||||||
|
|
||||||
p := meta.(*ResourceProvider)
|
client := meta.(*heroku.Client)
|
||||||
client := p.client
|
|
||||||
|
|
||||||
// Merge the diff into the state so that we have all the attributes
|
app := d.Get("app").(string)
|
||||||
// properly.
|
plan := d.Get("plan").(string)
|
||||||
rs := s.MergeDiff(d)
|
|
||||||
|
|
||||||
app := rs.Attributes["app"]
|
|
||||||
plan := rs.Attributes["plan"]
|
|
||||||
opts := heroku.AddonCreateOpts{}
|
opts := heroku.AddonCreateOpts{}
|
||||||
|
|
||||||
if attr, ok := rs.Attributes["config.#"]; ok && attr == "1" {
|
if v := d.Get("config"); v != nil {
|
||||||
vs := flatmap.Expand(
|
|
||||||
rs.Attributes, "config").([]interface{})
|
|
||||||
|
|
||||||
config := make(map[string]string)
|
config := make(map[string]string)
|
||||||
for k, v := range vs[0].(map[string]interface{}) {
|
for _, v := range v.([]interface{}) {
|
||||||
|
for k, v := range v.(map[string]interface{}) {
|
||||||
config[k] = v.(string)
|
config[k] = v.(string)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
opts.Config = &config
|
opts.Config = &config
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[DEBUG] Addon create configuration: %#v, %#v, %#v", app, plan, opts)
|
log.Printf("[DEBUG] Addon create configuration: %#v, %#v, %#v", app, plan, opts)
|
||||||
|
|
||||||
a, err := client.AddonCreate(app, plan, &opts)
|
a, err := client.AddonCreate(app, plan, &opts)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return s, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rs.ID = a.Id
|
d.SetId(a.Id)
|
||||||
log.Printf("[INFO] Addon ID: %s", rs.ID)
|
log.Printf("[INFO] Addon ID: %s", d.Id())
|
||||||
|
|
||||||
addon, err := resource_heroku_addon_retrieve(app, rs.ID, client)
|
return resourceHerokuAddonRead(d, meta)
|
||||||
if err != nil {
|
|
||||||
return rs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return resource_heroku_addon_update_state(rs, addon)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func resource_heroku_addon_update(
|
func resourceHerokuAddonRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
s *terraform.ResourceState,
|
client := meta.(*heroku.Client)
|
||||||
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)
|
|
||||||
|
|
||||||
|
addon, err := resource_heroku_addon_retrieve(
|
||||||
|
d.Get("app").(string), d.Id(), client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return s, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the new ID
|
d.Set("name", addon.Name)
|
||||||
rs.ID = ad.Id
|
d.Set("plan", addon.Plan.Name)
|
||||||
}
|
d.Set("provider_id", addon.ProviderId)
|
||||||
|
d.Set("config_vars", []interface{}{addon.ConfigVars})
|
||||||
addon, err := resource_heroku_addon_retrieve(app, rs.ID, client)
|
d.SetDependencies([]terraform.ResourceDependency{
|
||||||
|
terraform.ResourceDependency{ID: d.Get("app").(string)},
|
||||||
if err != nil {
|
})
|
||||||
return rs, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resource_heroku_addon_refresh(
|
func resourceHerokuAddonUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
s *terraform.ResourceState,
|
client := meta.(*heroku.Client)
|
||||||
meta interface{}) (*terraform.ResourceState, error) {
|
|
||||||
p := meta.(*ResourceProvider)
|
|
||||||
client := p.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 {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return resource_heroku_addon_update_state(s, app)
|
// Store the new ID
|
||||||
|
d.SetId(ad.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceHerokuAddonRead(d, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resource_heroku_addon_diff(
|
func resourceHerokuAddonDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
s *terraform.ResourceState,
|
client := meta.(*heroku.Client)
|
||||||
c *terraform.ResourceConfig,
|
|
||||||
meta interface{}) (*terraform.ResourceDiff, error) {
|
|
||||||
|
|
||||||
b := &diff.ResourceBuilder{
|
log.Printf("[INFO] Deleting Addon: %s", d.Id())
|
||||||
Attrs: map[string]diff.AttrType{
|
|
||||||
"app": diff.AttrTypeCreate,
|
|
||||||
"plan": diff.AttrTypeUpdate,
|
|
||||||
"config": diff.AttrTypeCreate,
|
|
||||||
},
|
|
||||||
|
|
||||||
ComputedAttrs: []string{
|
// Destroy the app
|
||||||
"provider_id",
|
err := client.AddonDelete(d.Get("app").(string), d.Id())
|
||||||
"config_vars",
|
if err != nil {
|
||||||
},
|
return fmt.Errorf("Error deleting addon: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.Diff(s, c)
|
d.SetId("")
|
||||||
}
|
return nil
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func resource_heroku_addon_retrieve(app string, id string, client *heroku.Client) (*heroku.Addon, error) {
|
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
|
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 {
|
func testAccCheckHerokuAddonDestroy(s *terraform.State) error {
|
||||||
client := testAccProvider.client
|
client := testAccProvider.Meta().(*heroku.Client)
|
||||||
|
|
||||||
for _, rs := range s.Resources {
|
for _, rs := range s.Resources {
|
||||||
if rs.Type != "heroku_addon" {
|
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")
|
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)
|
foundAddon, err := client.AddonInfo(rs.Attributes["app"], rs.ID)
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,8 @@ import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/bgentry/heroku-go"
|
"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/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
|
// type application is used to store all the details of a heroku app
|
||||||
|
@ -43,200 +40,167 @@ func (a *application) Update() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resource_heroku_app_create(
|
func resourceHerokuApp() *schema.Resource {
|
||||||
s *terraform.ResourceState,
|
return &schema.Resource{
|
||||||
d *terraform.ResourceDiff,
|
Create: resourceHerokuAppCreate,
|
||||||
meta interface{}) (*terraform.ResourceState, error) {
|
Read: resourceHerokuAppRead,
|
||||||
p := meta.(*ResourceProvider)
|
Update: resourceHerokuAppUpdate,
|
||||||
client := p.client
|
Delete: resourceHerokuAppDelete,
|
||||||
|
|
||||||
// Merge the diff into the state so that we have all the attributes
|
Schema: map[string]*schema.Schema{
|
||||||
// properly.
|
"name": &schema.Schema{
|
||||||
rs := s.MergeDiff(d)
|
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
|
// Build up our creation options
|
||||||
opts := heroku.AppCreateOpts{}
|
opts := heroku.AppCreateOpts{}
|
||||||
|
|
||||||
if attr := rs.Attributes["name"]; attr != "" {
|
if v := d.Get("name"); v != nil {
|
||||||
opts.Name = &attr
|
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 != "" {
|
log.Printf("[DEBUG] Creating Heroku app...")
|
||||||
opts.Region = &attr
|
|
||||||
}
|
|
||||||
|
|
||||||
if attr := rs.Attributes["stack"]; attr != "" {
|
|
||||||
opts.Stack = &attr
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("[DEBUG] App create configuration: %#v", opts)
|
|
||||||
|
|
||||||
a, err := client.AppCreate(&opts)
|
a, err := client.AppCreate(&opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return s, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rs.ID = a.Name
|
d.SetId(a.Name)
|
||||||
log.Printf("[INFO] App ID: %s", rs.ID)
|
log.Printf("[INFO] App ID: %s", d.Id())
|
||||||
|
|
||||||
if attr, ok := rs.Attributes["config_vars.#"]; ok && attr == "1" {
|
if v := d.Get("config_vars"); v != nil {
|
||||||
vs := flatmap.Expand(
|
err = update_config_vars(d.Id(), v.([]interface{}), client)
|
||||||
rs.Attributes, "config_vars").([]interface{})
|
|
||||||
|
|
||||||
err = update_config_vars(rs.ID, vs, client)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rs, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := resource_heroku_app_retrieve(rs.ID, client)
|
return resourceHerokuAppRead(d, meta)
|
||||||
if err != nil {
|
|
||||||
return rs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return resource_heroku_app_update_state(rs, app)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func resource_heroku_app_update(
|
func resourceHerokuAppRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
s *terraform.ResourceState,
|
client := meta.(*heroku.Client)
|
||||||
d *terraform.ResourceDiff,
|
app, err := resource_heroku_app_retrieve(d.Id(), client)
|
||||||
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 {
|
if err != nil {
|
||||||
return s, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the new ID
|
d.Set("name", app.App.Name)
|
||||||
rs.ID = renamedApp.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})
|
||||||
|
|
||||||
attr, ok := s.Attributes["config_vars.#"]
|
// We know that the hostname on heroku will be the name+herokuapp.com
|
||||||
|
// You need this to do things like create DNS CNAME records
|
||||||
// If the config var block was removed, nuke all config vars
|
d.Set("heroku_hostname", fmt.Sprintf("%s.herokuapp.com", app.App.Name))
|
||||||
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)
|
|
||||||
if err != nil {
|
|
||||||
return rs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return resource_heroku_app_update_state(rs, app)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resource_heroku_app_refresh(
|
func resourceHerokuAppUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
s *terraform.ResourceState,
|
client := meta.(*heroku.Client)
|
||||||
meta interface{}) (*terraform.ResourceState, error) {
|
|
||||||
p := meta.(*ResourceProvider)
|
|
||||||
client := p.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 {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return resource_heroku_app_update_state(s, app)
|
// Store the new ID
|
||||||
|
d.SetId(renamedApp.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the config vars. If we have none, then set it to the empty
|
||||||
|
// list so that they're properly removed.
|
||||||
|
v := d.Get("config_vars")
|
||||||
|
if v == nil {
|
||||||
|
v = []interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := update_config_vars(d.Id(), v.([]interface{}), client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceHerokuAppRead(d, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resource_heroku_app_diff(
|
func resourceHerokuAppDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
s *terraform.ResourceState,
|
client := meta.(*heroku.Client)
|
||||||
c *terraform.ResourceConfig,
|
|
||||||
meta interface{}) (*terraform.ResourceDiff, error) {
|
|
||||||
|
|
||||||
b := &diff.ResourceBuilder{
|
log.Printf("[INFO] Deleting App: %s", d.Id())
|
||||||
Attrs: map[string]diff.AttrType{
|
err := client.AppDelete(d.Id())
|
||||||
"name": diff.AttrTypeUpdate,
|
if err != nil {
|
||||||
"region": diff.AttrTypeUpdate,
|
return fmt.Errorf("Error deleting App: %s", err)
|
||||||
"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)
|
d.SetId("")
|
||||||
}
|
return nil
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func resource_heroku_app_retrieve(id string, client *heroku.Client) (*application, error) {
|
func resource_heroku_app_retrieve(id string, client *heroku.Client) (*application, error) {
|
||||||
|
@ -251,18 +215,6 @@ func resource_heroku_app_retrieve(id string, client *heroku.Client) (*applicatio
|
||||||
return &app, nil
|
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) {
|
func retrieve_config_vars(id string, client *heroku.Client) (map[string]string, error) {
|
||||||
vars, err := client.ConfigVarInfo(id)
|
vars, err := client.ConfigVarInfo(id)
|
||||||
|
|
||||||
|
@ -273,21 +225,19 @@ func retrieve_config_vars(id string, client *heroku.Client) (map[string]string,
|
||||||
return vars, nil
|
return vars, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates the config vars for from an expanded (prior to assertion)
|
// Updates the config vars for from an expanded configuration.
|
||||||
// []map[string]string config
|
|
||||||
func update_config_vars(id string, vs []interface{}, client *heroku.Client) error {
|
func update_config_vars(id string, vs []interface{}, client *heroku.Client) error {
|
||||||
vars := make(map[string]*string)
|
vars := make(map[string]*string)
|
||||||
|
|
||||||
for k, v := range vs[0].(map[string]interface{}) {
|
for _, v := range vs {
|
||||||
|
for k, v := range v.(map[string]interface{}) {
|
||||||
val := v.(string)
|
val := v.(string)
|
||||||
vars[k] = &val
|
vars[k] = &val
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("[INFO] Updating config vars: *%#v", vars)
|
log.Printf("[INFO] Updating config vars: *%#v", vars)
|
||||||
|
if _, err := client.ConfigVarUpdate(id, vars); err != nil {
|
||||||
_, err := client.ConfigVarUpdate(id, vars)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error updating config vars: %s", err)
|
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 {
|
func testAccCheckHerokuAppDestroy(s *terraform.State) error {
|
||||||
client := testAccProvider.client
|
client := testAccProvider.Meta().(*heroku.Client)
|
||||||
|
|
||||||
for _, rs := range s.Resources {
|
for _, rs := range s.Resources {
|
||||||
if rs.Type != "heroku_app" {
|
if rs.Type != "heroku_app" {
|
||||||
|
@ -122,7 +122,7 @@ func testAccCheckHerokuAppDestroy(s *terraform.State) error {
|
||||||
|
|
||||||
func testAccCheckHerokuAppAttributes(app *heroku.App) resource.TestCheckFunc {
|
func testAccCheckHerokuAppAttributes(app *heroku.App) resource.TestCheckFunc {
|
||||||
return func(s *terraform.State) error {
|
return func(s *terraform.State) error {
|
||||||
client := testAccProvider.client
|
client := testAccProvider.Meta().(*heroku.Client)
|
||||||
|
|
||||||
if app.Region.Name != "us" {
|
if app.Region.Name != "us" {
|
||||||
return fmt.Errorf("Bad region: %s", app.Region.Name)
|
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 {
|
func testAccCheckHerokuAppAttributesUpdated(app *heroku.App) resource.TestCheckFunc {
|
||||||
return func(s *terraform.State) error {
|
return func(s *terraform.State) error {
|
||||||
client := testAccProvider.client
|
client := testAccProvider.Meta().(*heroku.Client)
|
||||||
|
|
||||||
if app.Name != "terraform-test-renamed" {
|
if app.Name != "terraform-test-renamed" {
|
||||||
return fmt.Errorf("Bad name: %s", app.Name)
|
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 {
|
func testAccCheckHerokuAppAttributesNoVars(app *heroku.App) resource.TestCheckFunc {
|
||||||
return func(s *terraform.State) error {
|
return func(s *terraform.State) error {
|
||||||
client := testAccProvider.client
|
client := testAccProvider.Meta().(*heroku.Client)
|
||||||
|
|
||||||
if app.Name != "terraform-test-app" {
|
if app.Name != "terraform-test-app" {
|
||||||
return fmt.Errorf("Bad name: %s", app.Name)
|
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")
|
return fmt.Errorf("No App Name is set")
|
||||||
}
|
}
|
||||||
|
|
||||||
client := testAccProvider.client
|
client := testAccProvider.Meta().(*heroku.Client)
|
||||||
|
|
||||||
foundApp, err := client.AppInfo(rs.ID)
|
foundApp, err := client.AppInfo(rs.ID)
|
||||||
|
|
||||||
|
|
|
@ -5,63 +5,64 @@ import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/bgentry/heroku-go"
|
"github.com/bgentry/heroku-go"
|
||||||
"github.com/hashicorp/terraform/helper/config"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
"github.com/hashicorp/terraform/helper/diff"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func resource_heroku_domain_create(
|
func resourceHerokuDomain() *schema.Resource {
|
||||||
s *terraform.ResourceState,
|
return &schema.Resource{
|
||||||
d *terraform.ResourceDiff,
|
Create: resourceHerokuDomainCreate,
|
||||||
meta interface{}) (*terraform.ResourceState, error) {
|
Read: resourceHerokuDomainRead,
|
||||||
p := meta.(*ResourceProvider)
|
Delete: resourceHerokuDomainDelete,
|
||||||
client := p.client
|
|
||||||
|
|
||||||
// Merge the diff into the state so that we have all the attributes
|
Schema: map[string]*schema.Schema{
|
||||||
// properly.
|
"hostname": &schema.Schema{
|
||||||
rs := s.MergeDiff(d)
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
app := rs.Attributes["app"]
|
"app": &schema.Schema{
|
||||||
hostname := rs.Attributes["hostname"]
|
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)
|
log.Printf("[DEBUG] Domain create configuration: %#v, %#v", app, hostname)
|
||||||
|
|
||||||
do, err := client.DomainCreate(app, hostname)
|
do, err := client.DomainCreate(app, hostname)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return s, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rs.ID = do.Id
|
d.SetId(do.Id)
|
||||||
rs.Attributes["hostname"] = do.Hostname
|
d.Set("hostname", do.Hostname)
|
||||||
rs.Attributes["cname"] = fmt.Sprintf("%s.herokuapp.com", app)
|
d.Set("cname", fmt.Sprintf("%s.herokuapp.com", app))
|
||||||
|
|
||||||
log.Printf("[INFO] Domain ID: %s", rs.ID)
|
log.Printf("[INFO] Domain ID: %s", d.Id())
|
||||||
|
return nil
|
||||||
return rs, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func resource_heroku_domain_update(
|
func resourceHerokuDomainDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
s *terraform.ResourceState,
|
client := meta.(*heroku.Client)
|
||||||
d *terraform.ResourceDiff,
|
|
||||||
meta interface{}) (*terraform.ResourceState, error) {
|
|
||||||
|
|
||||||
panic("Cannot update domain")
|
log.Printf("[INFO] Deleting Domain: %s", d.Id())
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
// Destroy the domain
|
||||||
|
err := client.DomainDelete(d.Get("app").(string), d.Id())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error deleting domain: %s", err)
|
return fmt.Errorf("Error deleting domain: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -69,58 +70,17 @@ func resource_heroku_domain_destroy(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resource_heroku_domain_refresh(
|
func resourceHerokuDomainRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
s *terraform.ResourceState,
|
client := meta.(*heroku.Client)
|
||||||
meta interface{}) (*terraform.ResourceState, error) {
|
|
||||||
p := meta.(*ResourceProvider)
|
|
||||||
client := p.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 {
|
if err != nil {
|
||||||
return nil, err
|
return fmt.Errorf("Error retrieving domain: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Attributes["hostname"] = domain.Hostname
|
d.Set("hostname", do.Hostname)
|
||||||
s.Attributes["cname"] = fmt.Sprintf("%s.herokuapp.com", s.Attributes["app"])
|
d.Set("cname", fmt.Sprintf("%s.herokuapp.com", app))
|
||||||
|
|
||||||
return s, nil
|
return 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{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ func TestAccHerokuDomain_Basic(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAccCheckHerokuDomainDestroy(s *terraform.State) error {
|
func testAccCheckHerokuDomainDestroy(s *terraform.State) error {
|
||||||
client := testAccProvider.client
|
client := testAccProvider.Meta().(*heroku.Client)
|
||||||
|
|
||||||
for _, rs := range s.Resources {
|
for _, rs := range s.Resources {
|
||||||
if rs.Type != "heroku_domain" {
|
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")
|
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)
|
foundDomain, err := client.DomainInfo(rs.Attributes["app"], rs.ID)
|
||||||
|
|
||||||
|
|
|
@ -5,63 +5,64 @@ import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/bgentry/heroku-go"
|
"github.com/bgentry/heroku-go"
|
||||||
"github.com/hashicorp/terraform/helper/config"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
"github.com/hashicorp/terraform/helper/diff"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func resource_heroku_drain_create(
|
func resourceHerokuDrain() *schema.Resource {
|
||||||
s *terraform.ResourceState,
|
return &schema.Resource{
|
||||||
d *terraform.ResourceDiff,
|
Create: resourceHerokuDrainCreate,
|
||||||
meta interface{}) (*terraform.ResourceState, error) {
|
Read: resourceHerokuDrainRead,
|
||||||
p := meta.(*ResourceProvider)
|
Delete: resourceHerokuDrainDelete,
|
||||||
client := p.client
|
|
||||||
|
|
||||||
// Merge the diff into the state so that we have all the attributes
|
Schema: map[string]*schema.Schema{
|
||||||
// properly.
|
"url": &schema.Schema{
|
||||||
rs := s.MergeDiff(d)
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
app := rs.Attributes["app"]
|
"app": &schema.Schema{
|
||||||
url := rs.Attributes["url"]
|
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)
|
log.Printf("[DEBUG] Drain create configuration: %#v, %#v", app, url)
|
||||||
|
|
||||||
dr, err := client.LogDrainCreate(app, url)
|
dr, err := client.LogDrainCreate(app, url)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return s, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rs.ID = dr.Id
|
d.SetId(dr.Id)
|
||||||
rs.Attributes["url"] = dr.URL
|
d.Set("url", dr.URL)
|
||||||
rs.Attributes["token"] = dr.Token
|
d.Set("token", dr.Token)
|
||||||
|
|
||||||
log.Printf("[INFO] Drain ID: %s", rs.ID)
|
log.Printf("[INFO] Drain ID: %s", d.Id())
|
||||||
|
return nil
|
||||||
return rs, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func resource_heroku_drain_update(
|
func resourceHerokuDrainDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
s *terraform.ResourceState,
|
client := meta.(*heroku.Client)
|
||||||
d *terraform.ResourceDiff,
|
|
||||||
meta interface{}) (*terraform.ResourceState, error) {
|
|
||||||
|
|
||||||
panic("Cannot update drain")
|
log.Printf("[INFO] Deleting drain: %s", d.Id())
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
// Destroy the drain
|
||||||
|
err := client.LogDrainDelete(d.Get("app").(string), d.Id())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error deleting drain: %s", err)
|
return fmt.Errorf("Error deleting drain: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -69,58 +70,16 @@ func resource_heroku_drain_destroy(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resource_heroku_drain_refresh(
|
func resourceHerokuDrainRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
s *terraform.ResourceState,
|
client := meta.(*heroku.Client)
|
||||||
meta interface{}) (*terraform.ResourceState, error) {
|
|
||||||
p := meta.(*ResourceProvider)
|
|
||||||
client := p.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 {
|
if err != nil {
|
||||||
return nil, err
|
return fmt.Errorf("Error retrieving drain: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Attributes["url"] = drain.URL
|
d.Set("url", dr.URL)
|
||||||
s.Attributes["token"] = drain.Token
|
d.Set("token", dr.Token)
|
||||||
|
|
||||||
return s, nil
|
return 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{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ func TestAccHerokuDrain_Basic(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAccCheckHerokuDrainDestroy(s *terraform.State) error {
|
func testAccCheckHerokuDrainDestroy(s *terraform.State) error {
|
||||||
client := testAccProvider.client
|
client := testAccProvider.Meta().(*heroku.Client)
|
||||||
|
|
||||||
for _, rs := range s.Resources {
|
for _, rs := range s.Resources {
|
||||||
if rs.Type != "heroku_drain" {
|
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")
|
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)
|
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -75,11 +75,13 @@ func NewContext(opts *ContextOpts) *Context {
|
||||||
|
|
||||||
// Calculate all the default variables
|
// Calculate all the default variables
|
||||||
defaultVars := make(map[string]string)
|
defaultVars := make(map[string]string)
|
||||||
|
if opts.Config != nil {
|
||||||
for _, v := range opts.Config.Variables {
|
for _, v := range opts.Config.Variables {
|
||||||
for k, val := range v.DefaultsMap() {
|
for k, val := range v.DefaultsMap() {
|
||||||
defaultVars[k] = val
|
defaultVars[k] = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &Context{
|
return &Context{
|
||||||
config: opts.Config,
|
config: opts.Config,
|
||||||
|
|
|
@ -89,34 +89,14 @@ func (c *ResourceConfig) CheckSet(keys []string) []error {
|
||||||
// The second return value is true if the get was successful. Get will
|
// The second return value is true if the get was successful. Get will
|
||||||
// not succeed if the value is being computed.
|
// not succeed if the value is being computed.
|
||||||
func (c *ResourceConfig) Get(k string) (interface{}, bool) {
|
func (c *ResourceConfig) Get(k string) (interface{}, bool) {
|
||||||
parts := strings.Split(k, ".")
|
// First try to get it from c.Config since that has interpolated values
|
||||||
|
result, ok := c.get(k, c.Config)
|
||||||
var current interface{} = c.Raw
|
if ok {
|
||||||
for _, part := range parts {
|
return result, ok
|
||||||
if current == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cv := reflect.ValueOf(current)
|
// Otherwise, just get it from the raw config
|
||||||
switch cv.Kind() {
|
return c.get(k, c.Raw)
|
||||||
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()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return current, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSet checks if the key in the configuration is set. A key is set if
|
// 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
|
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 {
|
func (c *ResourceConfig) interpolate(ctx *Context) error {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -3,6 +3,8 @@ package terraform
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestResource_Vars(t *testing.T) {
|
func TestResource_Vars(t *testing.T) {
|
||||||
|
@ -29,3 +31,49 @@ func TestResource_Vars(t *testing.T) {
|
||||||
t.Fatalf("bad: %#v", actual)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue