Merge pull request #201 from hashicorp/f-resource

High-Level Framework for Writing Providers and Resources
This commit is contained in:
Mitchell Hashimoto 2014-08-19 09:40:59 -07:00
commit 839f9d84c8
25 changed files with 4800 additions and 675 deletions

View File

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

View File

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

View File

@ -2,29 +2,35 @@ package heroku
import ( 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)
} }
} }

View File

@ -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.*",
},
}
}

View File

@ -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)

View File

@ -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)
} }

View File

@ -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)

View File

@ -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{},
}
} }

View File

@ -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)

View File

@ -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{},
}
} }

View File

@ -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)

View File

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

View File

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

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

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

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

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

View File

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

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

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

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

1019
helper/schema/schema_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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,

View File

@ -89,34 +89,14 @@ func (c *ResourceConfig) CheckSet(keys []string) []error {
// The second return value is true if the get was successful. Get will // 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

View File

@ -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)
}
}
}