diff --git a/builtin/providers/aws/resources.go b/builtin/providers/aws/resources.go index 687cd6479..f03689b14 100644 --- a/builtin/providers/aws/resources.go +++ b/builtin/providers/aws/resources.go @@ -39,6 +39,14 @@ func init() { Update: resource_aws_db_security_group_update, }, + "aws_eip": resource.Resource{ + ConfigValidator: resource_aws_eip_validation(), + Create: resource_aws_eip_create, + Destroy: resource_aws_eip_destroy, + Diff: resource_aws_eip_diff, + Refresh: resource_aws_eip_refresh, + }, + "aws_elb": resource.Resource{ ConfigValidator: resource_aws_elb_validation(), Create: resource_aws_elb_create, @@ -48,14 +56,6 @@ func init() { Refresh: resource_aws_elb_refresh, }, - "aws_eip": resource.Resource{ - ConfigValidator: resource_aws_eip_validation(), - Create: resource_aws_eip_create, - Destroy: resource_aws_eip_destroy, - Diff: resource_aws_eip_diff, - Refresh: resource_aws_eip_refresh, - }, - "aws_instance": resource.Resource{ Create: resource_aws_instance_create, Destroy: resource_aws_instance_destroy, @@ -112,14 +112,6 @@ func init() { Update: resource_aws_route_table_association_update, }, - "aws_route53_zone": resource.Resource{ - ConfigValidator: resource_aws_r53_zone_validation(), - Create: resource_aws_r53_zone_create, - Destroy: resource_aws_r53_zone_destroy, - Diff: resource_aws_r53_zone_diff, - Refresh: resource_aws_r53_zone_refresh, - }, - "aws_route53_record": resource.Resource{ ConfigValidator: resource_aws_r53_record_validation(), Create: resource_aws_r53_record_create, @@ -129,6 +121,14 @@ func init() { Update: resource_aws_r53_record_create, }, + "aws_route53_zone": resource.Resource{ + ConfigValidator: resource_aws_r53_zone_validation(), + Create: resource_aws_r53_zone_create, + Destroy: resource_aws_r53_zone_destroy, + Diff: resource_aws_r53_zone_diff, + Refresh: resource_aws_r53_zone_refresh, + }, + "aws_s3_bucket": resource.Resource{ ConfigValidator: resource_aws_s3_bucket_validation(), Create: resource_aws_s3_bucket_create, diff --git a/builtin/providers/digitalocean/resource_digitalocean_domain.go b/builtin/providers/digitalocean/resource_digitalocean_domain.go new file mode 100644 index 000000000..112b75bba --- /dev/null +++ b/builtin/providers/digitalocean/resource_digitalocean_domain.go @@ -0,0 +1,101 @@ +package digitalocean + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/terraform" + "github.com/pearkes/digitalocean" +) + +func resource_digitalocean_domain_create( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + // Merge the diff into the state so that we have all the attributes + // properly. + rs := s.MergeDiff(d) + + // Build up our creation options + opts := digitalocean.CreateDomain{ + Name: rs.Attributes["name"], + IPAddress: rs.Attributes["ip_address"], + } + + log.Printf("[DEBUG] Domain create configuration: %#v", opts) + + name, err := client.CreateDomain(&opts) + if err != nil { + return nil, fmt.Errorf("Error creating Domain: %s", err) + } + + rs.ID = name + log.Printf("[INFO] Domain Name: %s", name) + + return rs, nil +} + +func resource_digitalocean_domain_destroy( + s *terraform.ResourceState, + meta interface{}) error { + p := meta.(*ResourceProvider) + client := p.client + + log.Printf("[INFO] Deleting Domain: %s", s.ID) + + err := client.DestroyDomain(s.ID) + + if err != nil { + return fmt.Errorf("Error deleting Domain: %s", err) + } + + return nil +} + +func resource_digitalocean_domain_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + domain, err := client.RetrieveDomain(s.ID) + + if err != nil { + return s, fmt.Errorf("Error retrieving domain: %s", err) + } + + s.Attributes["name"] = domain.Name + + return s, nil +} + +func resource_digitalocean_domain_diff( + s *terraform.ResourceState, + c *terraform.ResourceConfig, + meta interface{}) (*terraform.ResourceDiff, error) { + + b := &diff.ResourceBuilder{ + Attrs: map[string]diff.AttrType{ + "name": diff.AttrTypeCreate, + "ip_address": diff.AttrTypeCreate, + }, + + ComputedAttrs: []string{}, + } + + return b.Diff(s, c) +} + +func resource_digitalocean_domain_validation() *config.Validator { + return &config.Validator{ + Required: []string{ + "name", + "ip_address", + }, + Optional: []string{}, + } +} diff --git a/builtin/providers/digitalocean/resource_digitalocean_domain_test.go b/builtin/providers/digitalocean/resource_digitalocean_domain_test.go new file mode 100644 index 000000000..153afd2cd --- /dev/null +++ b/builtin/providers/digitalocean/resource_digitalocean_domain_test.go @@ -0,0 +1,99 @@ +package digitalocean + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/pearkes/digitalocean" +) + +func TestAccDigitalOceanDomain_Basic(t *testing.T) { + var domain digitalocean.Domain + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanDomainDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckDigitalOceanDomainConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanDomainExists("digitalocean_domain.foobar", &domain), + testAccCheckDigitalOceanDomainAttributes(&domain), + resource.TestCheckResourceAttr( + "digitalocean_domain.foobar", "name", "foobar-test-terraform.com"), + resource.TestCheckResourceAttr( + "digitalocean_domain.foobar", "ip_address", "192.168.0.10"), + ), + }, + }, + }) +} + +func testAccCheckDigitalOceanDomainDestroy(s *terraform.State) error { + client := testAccProvider.client + + for _, rs := range s.Resources { + if rs.Type != "digitalocean_domain" { + continue + } + + // Try to find the domain + _, err := client.RetrieveDomain(rs.ID) + + if err == nil { + fmt.Errorf("Domain still exists") + } + } + + return nil +} + +func testAccCheckDigitalOceanDomainAttributes(domain *digitalocean.Domain) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if domain.Name != "foobar-test-terraform.com" { + return fmt.Errorf("Bad name: %s", domain.Name) + } + + return nil + } +} + +func testAccCheckDigitalOceanDomainExists(n string, domain *digitalocean.Domain) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.ID == "" { + return fmt.Errorf("No Record ID is set") + } + + client := testAccProvider.client + + foundDomain, err := client.RetrieveDomain(rs.ID) + + if err != nil { + return err + } + + if foundDomain.Name != rs.ID { + return fmt.Errorf("Record not found") + } + + *domain = foundDomain + + return nil + } +} + +const testAccCheckDigitalOceanDomainConfig_basic = ` +resource "digitalocean_domain" "foobar" { + name = "foobar-test-terraform.com" + ip_address = "192.168.0.10" +}` diff --git a/builtin/providers/digitalocean/resource_digitalocean_droplet.go b/builtin/providers/digitalocean/resource_digitalocean_droplet.go index 83e98eef0..875fd952b 100644 --- a/builtin/providers/digitalocean/resource_digitalocean_droplet.go +++ b/builtin/providers/digitalocean/resource_digitalocean_droplet.go @@ -81,6 +81,10 @@ func resource_digitalocean_droplet_create( droplet := dropletRaw.(*digitalocean.Droplet) + // Initialize the connection info + rs.ConnInfo["type"] = "ssh" + rs.ConnInfo["host"] = droplet.IPV4Address() + return resource_digitalocean_droplet_update_state(rs, droplet) } diff --git a/builtin/providers/digitalocean/resource_digitalocean_record.go b/builtin/providers/digitalocean/resource_digitalocean_record.go new file mode 100644 index 000000000..011f5875d --- /dev/null +++ b/builtin/providers/digitalocean/resource_digitalocean_record.go @@ -0,0 +1,184 @@ +package digitalocean + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/terraform" + "github.com/pearkes/digitalocean" +) + +func resource_digitalocean_record_create( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + // Merge the diff into the state so that we have all the attributes + // properly. + rs := s.MergeDiff(d) + + var err error + + newRecord := digitalocean.CreateRecord{ + Type: rs.Attributes["type"], + Name: rs.Attributes["name"], + Data: rs.Attributes["value"], + Priority: rs.Attributes["priority"], + Port: rs.Attributes["port"], + Weight: rs.Attributes["weight"], + } + + log.Printf("[DEBUG] record create configuration: %#v", newRecord) + + recId, err := client.CreateRecord(rs.Attributes["domain"], &newRecord) + + if err != nil { + return nil, fmt.Errorf("Failed to create record: %s", err) + } + + rs.ID = recId + log.Printf("[INFO] Record ID: %s", rs.ID) + + record, err := resource_digitalocean_record_retrieve(rs.Attributes["domain"], rs.ID, client) + if err != nil { + return nil, fmt.Errorf("Couldn't find record: %s", err) + } + + return resource_digitalocean_record_update_state(rs, record) +} + +func resource_digitalocean_record_update( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + rs := s.MergeDiff(d) + + updateRecord := digitalocean.UpdateRecord{} + + if attr, ok := d.Attributes["name"]; ok { + updateRecord.Name = attr.New + } + + log.Printf("[DEBUG] record update configuration: %#v", updateRecord) + + err := client.UpdateRecord(rs.Attributes["domain"], rs.ID, &updateRecord) + if err != nil { + return rs, fmt.Errorf("Failed to update record: %s", err) + } + + record, err := resource_digitalocean_record_retrieve(rs.Attributes["domain"], rs.ID, client) + if err != nil { + return rs, fmt.Errorf("Couldn't find record: %s", err) + } + + return resource_digitalocean_record_update_state(rs, record) +} + +func resource_digitalocean_record_destroy( + s *terraform.ResourceState, + meta interface{}) error { + p := meta.(*ResourceProvider) + client := p.client + + log.Printf("[INFO] Deleting record: %s, %s", s.Attributes["domain"], s.ID) + + err := client.DestroyRecord(s.Attributes["domain"], s.ID) + + if err != nil { + return fmt.Errorf("Error deleting record: %s", err) + } + + return nil +} + +func resource_digitalocean_record_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + rec, err := resource_digitalocean_record_retrieve(s.Attributes["domain"], s.ID, client) + if err != nil { + return nil, err + } + + return resource_digitalocean_record_update_state(s, rec) +} + +func resource_digitalocean_record_diff( + s *terraform.ResourceState, + c *terraform.ResourceConfig, + meta interface{}) (*terraform.ResourceDiff, error) { + + b := &diff.ResourceBuilder{ + Attrs: map[string]diff.AttrType{ + "domain": diff.AttrTypeCreate, + "name": diff.AttrTypeUpdate, + "type": diff.AttrTypeCreate, + "value": diff.AttrTypeCreate, + "priority": diff.AttrTypeCreate, + "port": diff.AttrTypeCreate, + "weight": diff.AttrTypeCreate, + }, + + ComputedAttrs: []string{ + "value", + "priority", + "weight", + "port", + }, + } + + return b.Diff(s, c) +} + +func resource_digitalocean_record_update_state( + s *terraform.ResourceState, + rec *digitalocean.Record) (*terraform.ResourceState, error) { + + s.Attributes["name"] = rec.Name + s.Attributes["type"] = rec.Type + s.Attributes["value"] = rec.Data + s.Attributes["weight"] = rec.StringWeight() + s.Attributes["priority"] = rec.StringPriority() + s.Attributes["port"] = rec.StringPort() + + + // We belong to a Domain + s.Dependencies = []terraform.ResourceDependency{ + terraform.ResourceDependency{ID: s.Attributes["domain"]}, + } + + return s, nil +} + +func resource_digitalocean_record_retrieve(domain string, id string, client *digitalocean.Client) (*digitalocean.Record, error) { + record, err := client.RetrieveRecord(domain, id) + if err != nil { + return nil, err + } + + return &record, nil +} + +func resource_digitalocean_record_validation() *config.Validator { + return &config.Validator{ + Required: []string{ + "type", + "domain", + }, + Optional: []string{ + "value", + "name", + "weight", + "port", + "priority", + }, + } +} diff --git a/builtin/providers/digitalocean/resource_digitalocean_record_test.go b/builtin/providers/digitalocean/resource_digitalocean_record_test.go new file mode 100644 index 000000000..4b1634bbe --- /dev/null +++ b/builtin/providers/digitalocean/resource_digitalocean_record_test.go @@ -0,0 +1,175 @@ +package digitalocean + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/pearkes/digitalocean" +) + +func TestAccDigitalOceanRecord_Basic(t *testing.T) { + var record digitalocean.Record + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanRecordDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckDigitalOceanRecordConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanRecordExists("digitalocean_record.foobar", &record), + testAccCheckDigitalOceanRecordAttributes(&record), + resource.TestCheckResourceAttr( + "digitalocean_record.foobar", "name", "terraform"), + resource.TestCheckResourceAttr( + "digitalocean_record.foobar", "domain", "foobar-test-terraform.com"), + resource.TestCheckResourceAttr( + "digitalocean_record.foobar", "value", "192.168.0.10"), + ), + }, + }, + }) +} + +func TestAccDigitalOceanRecord_Updated(t *testing.T) { + var record digitalocean.Record + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanRecordDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckDigitalOceanRecordConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanRecordExists("digitalocean_record.foobar", &record), + testAccCheckDigitalOceanRecordAttributes(&record), + resource.TestCheckResourceAttr( + "digitalocean_record.foobar", "name", "terraform"), + resource.TestCheckResourceAttr( + "digitalocean_record.foobar", "domain", "foobar-test-terraform.com"), + resource.TestCheckResourceAttr( + "digitalocean_record.foobar", "value", "192.168.0.10"), + resource.TestCheckResourceAttr( + "digitalocean_record.foobar", "type", "A"), + ), + }, + resource.TestStep{ + Config: testAccCheckDigitalOceanRecordConfig_new_value, + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanRecordExists("digitalocean_record.foobar", &record), + testAccCheckDigitalOceanRecordAttributesUpdated(&record), + resource.TestCheckResourceAttr( + "digitalocean_record.foobar", "name", "terraform"), + resource.TestCheckResourceAttr( + "digitalocean_record.foobar", "domain", "foobar-test-terraform.com"), + resource.TestCheckResourceAttr( + "digitalocean_record.foobar", "value", "192.168.0.11"), + resource.TestCheckResourceAttr( + "digitalocean_record.foobar", "type", "A"), + ), + }, + }, + }) +} + +func testAccCheckDigitalOceanRecordDestroy(s *terraform.State) error { + client := testAccProvider.client + + for _, rs := range s.Resources { + if rs.Type != "digitalocean_record" { + continue + } + + _, err := client.RetrieveRecord(rs.Attributes["domain"], rs.ID) + + if err == nil { + return fmt.Errorf("Record still exists") + } + } + + return nil +} + +func testAccCheckDigitalOceanRecordAttributes(record *digitalocean.Record) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if record.Data != "192.168.0.10" { + return fmt.Errorf("Bad value: %s", record.Data) + } + + return nil + } +} + +func testAccCheckDigitalOceanRecordAttributesUpdated(record *digitalocean.Record) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if record.Data != "192.168.0.11" { + return fmt.Errorf("Bad value: %s", record.Data) + } + + return nil + } +} + +func testAccCheckDigitalOceanRecordExists(n string, record *digitalocean.Record) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.ID == "" { + return fmt.Errorf("No Record ID is set") + } + + client := testAccProvider.client + + foundRecord, err := client.RetrieveRecord(rs.Attributes["domain"], rs.ID) + + if err != nil { + return err + } + + if foundRecord.StringId() != rs.ID { + return fmt.Errorf("Record not found") + } + + *record = foundRecord + + return nil + } +} + +const testAccCheckDigitalOceanRecordConfig_basic = ` +resource "digitalocean_domain" "foobar" { + name = "foobar-test-terraform.com" + ip_address = "192.168.0.10" +} + +resource "digitalocean_record" "foobar" { + domain = "${digitalocean_domain.foobar.name}" + + name = "terraform" + value = "192.168.0.10" + type = "A" +}` + +const testAccCheckDigitalOceanRecordConfig_new_value = ` +resource "digitalocean_domain" "foobar" { + name = "foobar-test-terraform.com" + ip_address = "192.168.0.10" +} + +resource "digitalocean_record" "foobar" { + domain = "${digitalocean_domain.foobar.name}" + + name = "terraform" + value = "192.168.0.11" + type = "A" +}` diff --git a/builtin/providers/digitalocean/resources.go b/builtin/providers/digitalocean/resources.go index 75b396c52..87cdacbc0 100644 --- a/builtin/providers/digitalocean/resources.go +++ b/builtin/providers/digitalocean/resources.go @@ -11,6 +11,14 @@ var resourceMap *resource.Map func init() { resourceMap = &resource.Map{ Mapping: map[string]resource.Resource{ + "digitalocean_domain": resource.Resource{ + ConfigValidator: resource_digitalocean_domain_validation(), + Create: resource_digitalocean_domain_create, + Destroy: resource_digitalocean_domain_destroy, + Diff: resource_digitalocean_domain_diff, + Refresh: resource_digitalocean_domain_refresh, + }, + "digitalocean_droplet": resource.Resource{ ConfigValidator: resource_digitalocean_droplet_validation(), Create: resource_digitalocean_droplet_create, @@ -19,6 +27,15 @@ func init() { Refresh: resource_digitalocean_droplet_refresh, Update: resource_digitalocean_droplet_update, }, + + "digitalocean_record": resource.Resource{ + ConfigValidator: resource_digitalocean_record_validation(), + Create: resource_digitalocean_record_create, + Destroy: resource_digitalocean_record_destroy, + Update: resource_digitalocean_record_update, + Diff: resource_digitalocean_record_diff, + Refresh: resource_digitalocean_record_refresh, + }, }, } } diff --git a/builtin/providers/dnsimple/config.go b/builtin/providers/dnsimple/config.go index dfec0f2e1..5f8c36e0b 100644 --- a/builtin/providers/dnsimple/config.go +++ b/builtin/providers/dnsimple/config.go @@ -1,10 +1,11 @@ package dnsimple import ( + "fmt" "log" "os" - "github.com/rubyist/go-dnsimple" + "github.com/pearkes/dnsimple" ) type Config struct { @@ -14,7 +15,7 @@ type Config struct { // Client() returns a new client for accessing heroku. // -func (c *Config) Client() (*dnsimple.DNSimpleClient, error) { +func (c *Config) Client() (*dnsimple.Client, error) { // If we have env vars set (like in the acc) tests, // we need to override the values passed in here. @@ -25,7 +26,11 @@ func (c *Config) Client() (*dnsimple.DNSimpleClient, error) { c.Token = v } - client := dnsimple.NewClient(c.Token, c.Email) + client, err := dnsimple.NewClient(c.Email, c.Token) + + if err != nil { + return nil, fmt.Errorf("Error setting up client: %s", err) + } log.Printf("[INFO] DNSimple Client configured for user: %s", client.Email) diff --git a/builtin/providers/dnsimple/resource_dnsimple_record.go b/builtin/providers/dnsimple/resource_dnsimple_record.go index 881b28a71..611cc3606 100644 --- a/builtin/providers/dnsimple/resource_dnsimple_record.go +++ b/builtin/providers/dnsimple/resource_dnsimple_record.go @@ -3,12 +3,11 @@ package dnsimple import ( "fmt" "log" - "strconv" "github.com/hashicorp/terraform/helper/config" "github.com/hashicorp/terraform/helper/diff" "github.com/hashicorp/terraform/terraform" - "github.com/rubyist/go-dnsimple" + "github.com/pearkes/dnsimple" ) func resource_dnsimple_record_create( @@ -24,42 +23,74 @@ func resource_dnsimple_record_create( var err error - newRecord := dnsimple.Record{ - Name: rs.Attributes["name"], - Content: rs.Attributes["value"], - RecordType: rs.Attributes["type"], + newRecord := dnsimple.ChangeRecord{ + Name: rs.Attributes["name"], + Value: rs.Attributes["value"], + Type: rs.Attributes["type"], } if attr, ok := rs.Attributes["ttl"]; ok { - newRecord.TTL, err = strconv.Atoi(attr) - if err != nil { - return nil, err - } + newRecord.Ttl = attr } log.Printf("[DEBUG] record create configuration: %#v", newRecord) - rec, err := client.CreateRecord(rs.Attributes["domain"], newRecord) + recId, err := client.CreateRecord(rs.Attributes["domain"], &newRecord) if err != nil { return nil, fmt.Errorf("Failed to create record: %s", err) } - rs.ID = strconv.Itoa(rec.Id) - + rs.ID = recId log.Printf("[INFO] record ID: %s", rs.ID) - return resource_dnsimple_record_update_state(rs, &rec) + record, err := resource_dnsimple_record_retrieve(rs.Attributes["domain"], rs.ID, client) + if err != nil { + return nil, fmt.Errorf("Couldn't find record: %s", err) + } + + return resource_dnsimple_record_update_state(rs, record) } func resource_dnsimple_record_update( s *terraform.ResourceState, d *terraform.ResourceDiff, meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + rs := s.MergeDiff(d) - panic("Cannot update record") + updateRecord := dnsimple.ChangeRecord{} - return nil, nil + if attr, ok := d.Attributes["name"]; ok { + updateRecord.Name = attr.New + } + + if attr, ok := d.Attributes["value"]; ok { + updateRecord.Value = attr.New + } + + if attr, ok := d.Attributes["type"]; ok { + updateRecord.Type = attr.New + } + + if attr, ok := d.Attributes["ttl"]; ok { + updateRecord.Ttl = attr.New + } + + log.Printf("[DEBUG] record update configuration: %#v", updateRecord) + + _, err := client.UpdateRecord(rs.Attributes["domain"], rs.ID, &updateRecord) + if err != nil { + return rs, fmt.Errorf("Failed to update record: %s", err) + } + + record, err := resource_dnsimple_record_retrieve(rs.Attributes["domain"], rs.ID, client) + if err != nil { + return rs, fmt.Errorf("Couldn't find record: %s", err) + } + + return resource_dnsimple_record_update_state(rs, record) } func resource_dnsimple_record_destroy( @@ -68,14 +99,10 @@ func resource_dnsimple_record_destroy( p := meta.(*ResourceProvider) client := p.client - log.Printf("[INFO] Deleting record: %s", s.ID) + log.Printf("[INFO] Deleting record: %s, %s", s.Attributes["domain"], s.ID) - rec, err := resource_dnsimple_record_retrieve(s.Attributes["domain"], s.ID, client) - if err != nil { - return err - } + err := client.DestroyRecord(s.Attributes["domain"], s.ID) - err = rec.Delete(client) if err != nil { return fmt.Errorf("Error deleting record: %s", err) } @@ -89,7 +116,7 @@ func resource_dnsimple_record_refresh( p := meta.(*ResourceProvider) client := p.client - rec, err := resource_dnsimple_record_retrieve(s.Attributes["app"], s.ID, client) + rec, err := resource_dnsimple_record_retrieve(s.Attributes["domain"], s.ID, client) if err != nil { return nil, err } @@ -105,15 +132,16 @@ func resource_dnsimple_record_diff( b := &diff.ResourceBuilder{ Attrs: map[string]diff.AttrType{ "domain": diff.AttrTypeCreate, - "name": diff.AttrTypeCreate, + "name": diff.AttrTypeUpdate, "value": diff.AttrTypeUpdate, - "ttl": diff.AttrTypeCreate, + "ttl": diff.AttrTypeUpdate, "type": diff.AttrTypeUpdate, }, ComputedAttrs: []string{ "priority", "domain_id", + "ttl", }, } @@ -127,25 +155,20 @@ func resource_dnsimple_record_update_state( s.Attributes["name"] = rec.Name s.Attributes["value"] = rec.Content s.Attributes["type"] = rec.RecordType - s.Attributes["ttl"] = strconv.Itoa(rec.TTL) - s.Attributes["priority"] = strconv.Itoa(rec.Priority) - s.Attributes["domain_id"] = strconv.Itoa(rec.DomainId) + s.Attributes["ttl"] = rec.StringTtl() + s.Attributes["priority"] = rec.StringPrio() + s.Attributes["domain_id"] = rec.StringDomainId() return s, nil } -func resource_dnsimple_record_retrieve(domain string, id string, client *dnsimple.DNSimpleClient) (*dnsimple.Record, error) { - intId, err := strconv.Atoi(id) +func resource_dnsimple_record_retrieve(domain string, id string, client *dnsimple.Client) (*dnsimple.Record, error) { + record, err := client.RetrieveRecord(domain, id) if err != nil { return nil, err } - record, err := client.RetrieveRecord(domain, intId) - if err != nil { - return nil, fmt.Errorf("Error retrieving record: %s", err) - } - - return &record, nil + return record, nil } func resource_dnsimple_record_validation() *config.Validator { diff --git a/builtin/providers/dnsimple/resource_dnsimple_record_test.go b/builtin/providers/dnsimple/resource_dnsimple_record_test.go index 70217d35e..2bfba5190 100644 --- a/builtin/providers/dnsimple/resource_dnsimple_record_test.go +++ b/builtin/providers/dnsimple/resource_dnsimple_record_test.go @@ -3,12 +3,11 @@ package dnsimple import ( "fmt" "os" - "strconv" "testing" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "github.com/rubyist/go-dnsimple" + "github.com/pearkes/dnsimple" ) func TestAccDNSimpleRecord_Basic(t *testing.T) { @@ -37,6 +36,45 @@ func TestAccDNSimpleRecord_Basic(t *testing.T) { }) } +func TestAccDNSimpleRecord_Updated(t *testing.T) { + var record dnsimple.Record + domain := os.Getenv("DNSIMPLE_DOMAIN") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDNSimpleRecordDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: fmt.Sprintf(testAccCheckDNSimpleRecordConfig_basic, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckDNSimpleRecordExists("dnsimple_record.foobar", &record), + testAccCheckDNSimpleRecordAttributes(&record), + resource.TestCheckResourceAttr( + "dnsimple_record.foobar", "name", "terraform"), + resource.TestCheckResourceAttr( + "dnsimple_record.foobar", "domain", domain), + resource.TestCheckResourceAttr( + "dnsimple_record.foobar", "value", "192.168.0.10"), + ), + }, + resource.TestStep{ + Config: fmt.Sprintf(testAccCheckDNSimpleRecordConfig_new_value, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckDNSimpleRecordExists("dnsimple_record.foobar", &record), + testAccCheckDNSimpleRecordAttributesUpdated(&record), + resource.TestCheckResourceAttr( + "dnsimple_record.foobar", "name", "terraform"), + resource.TestCheckResourceAttr( + "dnsimple_record.foobar", "domain", domain), + resource.TestCheckResourceAttr( + "dnsimple_record.foobar", "value", "192.168.0.11"), + ), + }, + }, + }) +} + func testAccCheckDNSimpleRecordDestroy(s *terraform.State) error { client := testAccProvider.client @@ -45,12 +83,7 @@ func testAccCheckDNSimpleRecordDestroy(s *terraform.State) error { continue } - intId, err := strconv.Atoi(rs.ID) - if err != nil { - return err - } - - _, err = client.RetrieveRecord(rs.Attributes["domain"], intId) + _, err := client.RetrieveRecord(rs.Attributes["domain"], rs.ID) if err == nil { return fmt.Errorf("Record still exists") @@ -71,6 +104,17 @@ func testAccCheckDNSimpleRecordAttributes(record *dnsimple.Record) resource.Test } } +func testAccCheckDNSimpleRecordAttributesUpdated(record *dnsimple.Record) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if record.Content != "192.168.0.11" { + return fmt.Errorf("Bad content: %s", record.Content) + } + + return nil + } +} + func testAccCheckDNSimpleRecordExists(n string, record *dnsimple.Record) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.Resources[n] @@ -85,22 +129,17 @@ func testAccCheckDNSimpleRecordExists(n string, record *dnsimple.Record) resourc client := testAccProvider.client - intId, err := strconv.Atoi(rs.ID) - if err != nil { - return err - } - - foundRecord, err := client.RetrieveRecord(rs.Attributes["domain"], intId) + foundRecord, err := client.RetrieveRecord(rs.Attributes["domain"], rs.ID) if err != nil { return err } - if strconv.Itoa(foundRecord.Id) != rs.ID { + if foundRecord.StringId() != rs.ID { return fmt.Errorf("Record not found") } - *record = foundRecord + *record = *foundRecord return nil } @@ -115,3 +154,13 @@ resource "dnsimple_record" "foobar" { type = "A" ttl = 3600 }` + +const testAccCheckDNSimpleRecordConfig_new_value = ` +resource "dnsimple_record" "foobar" { + domain = "%s" + + name = "terraform" + value = "192.168.0.11" + type = "A" + ttl = 3600 +}` diff --git a/builtin/providers/dnsimple/resource_provider.go b/builtin/providers/dnsimple/resource_provider.go index 478acb470..d2b4abba4 100644 --- a/builtin/providers/dnsimple/resource_provider.go +++ b/builtin/providers/dnsimple/resource_provider.go @@ -5,13 +5,13 @@ import ( "github.com/hashicorp/terraform/helper/config" "github.com/hashicorp/terraform/terraform" - "github.com/rubyist/go-dnsimple" + "github.com/pearkes/dnsimple" ) type ResourceProvider struct { Config Config - client *dnsimple.DNSimpleClient + client *dnsimple.Client } func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) { diff --git a/builtin/providers/dnsimple/resource_provider_test.go b/builtin/providers/dnsimple/resource_provider_test.go index 5f0bd5f25..4867c1ebd 100644 --- a/builtin/providers/dnsimple/resource_provider_test.go +++ b/builtin/providers/dnsimple/resource_provider_test.go @@ -75,6 +75,6 @@ func testAccPreCheck(t *testing.T) { } if v := os.Getenv("DNSIMPLE_DOMAIN"); v == "" { - t.Fatal("DNSIMPLE_DOMAIN must be set for acceptance tests. The domain is used to create and destroy record against.") + t.Fatal("DNSIMPLE_DOMAIN must be set for acceptance tests. The domain is used to ` and destroy record against.") } } diff --git a/command/format_state.go b/command/format_state.go index a7df48291..d21d036e9 100644 --- a/command/format_state.go +++ b/command/format_state.go @@ -41,7 +41,7 @@ func FormatState(s *terraform.State, c *colorstring.Colorize) string { taintStr := "" if s.Tainted != nil { - if _, ok := s.Tainted[id]; ok { + if _, ok := s.Tainted[k]; ok { taintStr = " (tainted)" } } diff --git a/command/meta.go b/command/meta.go index 297557e7c..d33831e44 100644 --- a/command/meta.go +++ b/command/meta.go @@ -112,7 +112,6 @@ func (m *Meta) contextOpts() *terraform.ContextOpts { opts.Hooks[0] = m.uiHook() copy(opts.Hooks[1:], m.ContextOpts.Hooks) copy(opts.Hooks[len(m.ContextOpts.Hooks)+1:], m.extraHooks) - println(fmt.Sprintf("%#v", opts.Hooks)) if len(m.variables) > 0 { vs := make(map[string]string) diff --git a/config.go b/config.go index 29a30e66c..b12c7ce64 100644 --- a/config.go +++ b/config.go @@ -108,31 +108,10 @@ func (c *Config) ProviderFactories() map[string]terraform.ResourceProviderFactor } func (c *Config) providerFactory(path string) terraform.ResourceProviderFactory { - originalPath := path - return func() (terraform.ResourceProvider, error) { - // First look for the provider on the PATH. - path, err := exec.LookPath(path) - if err != nil { - // If that doesn't work, look for it in the same directory - // as the executable that is running. - exePath, err := osext.Executable() - if err == nil { - path = filepath.Join( - filepath.Dir(exePath), - filepath.Base(originalPath)) - } - } - - // If we still don't have a path set, then set it to the - // original path and let any errors that happen bubble out. - if path == "" { - path = originalPath - } - // Build the plugin client configuration and init the plugin var config plugin.ClientConfig - config.Cmd = exec.Command(path) + config.Cmd = pluginCmd(path) config.Managed = true client := plugin.NewClient(&config) @@ -168,31 +147,10 @@ func (c *Config) ProvisionerFactories() map[string]terraform.ResourceProvisioner } func (c *Config) provisionerFactory(path string) terraform.ResourceProvisionerFactory { - originalPath := path - return func() (terraform.ResourceProvisioner, error) { - // First look for the provider on the PATH. - path, err := exec.LookPath(path) - if err != nil { - // If that doesn't work, look for it in the same directory - // as the executable that is running. - exePath, err := osext.Executable() - if err == nil { - path = filepath.Join( - filepath.Dir(exePath), - filepath.Base(originalPath)) - } - } - - // If we still don't have a path set, then set it to the - // original path and let any errors that happen bubble out. - if path == "" { - path = originalPath - } - // Build the plugin client configuration and init the plugin var config plugin.ClientConfig - config.Cmd = exec.Command(path) + config.Cmd = pluginCmd(path) config.Managed = true client := plugin.NewClient(&config) @@ -214,3 +172,29 @@ func (c *Config) provisionerFactory(path string) terraform.ResourceProvisionerFa }, nil } } + +func pluginCmd(path string) *exec.Cmd { + originalPath := path + + // First look for the provider on the PATH. + path, err := exec.LookPath(path) + if err != nil { + // If that doesn't work, look for it in the same directory + // as the executable that is running. + exePath, err := osext.Executable() + if err == nil { + path = filepath.Join( + filepath.Dir(exePath), + filepath.Base(originalPath)) + } + } + + // If we still don't have a path set, then set it to the + // original path and let any errors that happen bubble out. + if path == "" { + path = originalPath + } + + // Build the command to execute the plugin + return exec.Command(path) +} diff --git a/depgraph/graph.go b/depgraph/graph.go index acca4ce6c..62b39572f 100644 --- a/depgraph/graph.go +++ b/depgraph/graph.go @@ -232,6 +232,15 @@ CHECK_CYCLES: } } + // Check for loops to yourself + for _, n := range g.Nouns { + for _, d := range n.Deps { + if d.Source == d.Target { + vErr.Cycles = append(vErr.Cycles, []*Noun{n}) + } + } + } + // Return the detailed error if vErr.MissingRoot || vErr.Unreachable != nil || vErr.Cycles != nil { return vErr diff --git a/flatmap/expand.go b/flatmap/expand.go index 87bc9b5da..2b689281e 100644 --- a/flatmap/expand.go +++ b/flatmap/expand.go @@ -11,9 +11,7 @@ import ( func Expand(m map[string]string, key string) interface{} { // If the key is exactly a key in the map, just return it if v, ok := m[key]; ok { - if num, err := strconv.ParseInt(v, 0, 0); err == nil { - return int(num) - } else if v == "true" { + if v == "true" { return true } else if v == "false" { return false diff --git a/flatmap/expand_test.go b/flatmap/expand_test.go index baf68e6a9..061e26897 100644 --- a/flatmap/expand_test.go +++ b/flatmap/expand_test.go @@ -44,7 +44,7 @@ func TestExpand(t *testing.T) { Output: []interface{}{ map[string]interface{}{ "name": "bar", - "port": 3000, + "port": "3000", "enabled": true, }, }, @@ -63,8 +63,8 @@ func TestExpand(t *testing.T) { map[string]interface{}{ "name": "bar", "ports": []interface{}{ - 1, - 2, + "1", + "2", }, }, }, diff --git a/plugin/client.go b/plugin/client.go index edc755f2c..f72ab52c0 100644 --- a/plugin/client.go +++ b/plugin/client.go @@ -11,6 +11,7 @@ import ( "net/rpc" "os" "os/exec" + "path/filepath" "strings" "sync" "time" @@ -217,7 +218,7 @@ func (c *Client) Start() (addr net.Addr, err error) { cmd.Stderr = stderr_w cmd.Stdout = stdout_w - log.Printf("Starting plugin: %s %#v", cmd.Path, cmd.Args) + log.Printf("[DEBUG] Starting plugin: %s %#v", cmd.Path, cmd.Args) err = cmd.Start() if err != nil { return @@ -248,7 +249,7 @@ func (c *Client) Start() (addr net.Addr, err error) { cmd.Wait() // Log and make sure to flush the logs write away - log.Printf("%s: plugin process exited\n", cmd.Path) + log.Printf("[DEBUG] %s: plugin process exited\n", cmd.Path) os.Stderr.Sync() // Mark that we exited @@ -295,7 +296,7 @@ func (c *Client) Start() (addr net.Addr, err error) { timeout := time.After(c.config.StartTimeout) // Start looking for the address - log.Printf("Waiting for RPC address for: %s", cmd.Path) + log.Printf("[DEBUG] Waiting for RPC address for: %s", cmd.Path) select { case <-timeout: err = errors.New("timeout while waiting for plugin to start") @@ -343,7 +344,7 @@ func (c *Client) logStderr(r io.Reader) { c.config.Stderr.Write([]byte(line)) line = strings.TrimRightFunc(line, unicode.IsSpace) - log.Printf("%s: %s", c.config.Cmd.Path, line) + log.Printf("%s: %s", filepath.Base(c.config.Cmd.Path), line) } if err == io.EOF { diff --git a/terraform/context.go b/terraform/context.go index 1e3acc0d4..eed5ecc46 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -121,7 +121,9 @@ func (c *Context) Apply() (*State, error) { c.state = c.state.deepcopy() // Walk + log.Printf("[INFO] Apply walk starting") err = g.Walk(c.applyWalkFn()) + log.Printf("[INFO] Apply walk complete") // Prune the state so that we have as clean a state as possible c.state.prune() @@ -565,6 +567,16 @@ func (c *Context) applyWalkFn() depgraph.WalkFunc { } } + // Update the resulting diff + c.sl.Lock() + if rs.ID == "" { + delete(c.state.Resources, r.Id) + delete(c.state.Tainted, r.Id) + } else { + c.state.Resources[r.Id] = rs + } + c.sl.Unlock() + // Invoke any provisioners we have defined. This is only done // if the resource was created, as updates or deletes do not // invoke provisioners. @@ -579,21 +591,17 @@ func (c *Context) applyWalkFn() depgraph.WalkFunc { } } - // Update the resulting diff - c.sl.Lock() - if rs.ID == "" { - delete(c.state.Resources, r.Id) - } else { - c.state.Resources[r.Id] = rs + if tainted { + log.Printf("[DEBUG] %s: Marking as tainted", r.Id) - if tainted { - c.state.Tainted[r.Id] = struct{}{} - } + c.sl.Lock() + c.state.Tainted[r.Id] = struct{}{} + c.sl.Unlock() } - c.sl.Unlock() // Update the state for the resource itself r.State = rs + r.Tainted = tainted for _, h := range c.hooks { handleHook(h.PostApply(r.Id, r.State, applyerr)) @@ -716,6 +724,7 @@ func (c *Context) planWalkFn(result *Plan) depgraph.WalkFunc { if r.Tainted { // Tainted resources must also be destroyed + log.Printf("[DEBUG] %s: Tainted, marking for destroy", r.Id) diff.Destroy = true } diff --git a/terraform/context_test.go b/terraform/context_test.go index 6abdbbb24..3516bc5cd 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -550,6 +550,52 @@ func TestContextApply_provisionerFail(t *testing.T) { } } +func TestContextApply_provisionerResourceRef(t *testing.T) { + c := testConfig(t, "apply-provisioner-resource-ref") + p := testProvider("aws") + pr := testProvisioner() + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + pr.ApplyFn = func(rs *ResourceState, c *ResourceConfig) error { + val, ok := c.Config["foo"] + if !ok || val != "2" { + t.Fatalf("bad value for foo: %v %#v", val, c) + } + + return nil + } + + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Provisioners: map[string]ResourceProvisionerFactory{ + "shell": testProvisionerFuncFixed(pr), + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyProvisionerResourceRefStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } + + // Verify apply was invoked + if !pr.ApplyCalled { + t.Fatalf("provisioner not invoked") + } +} + func TestContextApply_outputDiffVars(t *testing.T) { c := testConfig(t, "apply-good") p := testProvider("aws") diff --git a/terraform/graph.go b/terraform/graph.go index db3889a2e..5c9dfdfa2 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -3,6 +3,7 @@ package terraform import ( "errors" "fmt" + "log" "sort" "strings" @@ -100,6 +101,8 @@ func Graph(opts *GraphOpts) (*depgraph.Graph, error) { return nil, errors.New("Config is required for Graph") } + log.Printf("[DEBUG] Creating graph...") + g := new(depgraph.Graph) // First, build the initial resource graph. This only has the resources @@ -160,6 +163,10 @@ func Graph(opts *GraphOpts) (*depgraph.Graph, error) { return nil, err } + log.Printf( + "[DEBUG] Graph created and valid. %d nouns.", + len(g.Nouns)) + return g, nil } @@ -604,20 +611,20 @@ func graphAddVariableDeps(g *depgraph.Graph) { // Handle the resource variables vars = m.Config.RawConfig.Variables - nounAddVariableDeps(g, n, vars) + nounAddVariableDeps(g, n, vars, false) // Handle the variables of the resource provisioners for _, p := range m.Resource.Provisioners { vars = p.RawConfig.Variables - nounAddVariableDeps(g, n, vars) + nounAddVariableDeps(g, n, vars, true) vars = p.ConnInfo.Variables - nounAddVariableDeps(g, n, vars) + nounAddVariableDeps(g, n, vars, true) } case *GraphNodeResourceProvider: vars = m.Config.RawConfig.Variables - nounAddVariableDeps(g, n, vars) + nounAddVariableDeps(g, n, vars, false) default: continue @@ -627,7 +634,11 @@ func graphAddVariableDeps(g *depgraph.Graph) { // nounAddVariableDeps updates the dependencies of a noun given // a set of associated variable values -func nounAddVariableDeps(g *depgraph.Graph, n *depgraph.Noun, vars map[string]config.InterpolatedVariable) { +func nounAddVariableDeps( + g *depgraph.Graph, + n *depgraph.Noun, + vars map[string]config.InterpolatedVariable, + removeSelf bool) { for _, v := range vars { // Only resource variables impose dependencies rv, ok := v.(*config.ResourceVariable) @@ -641,6 +652,12 @@ func nounAddVariableDeps(g *depgraph.Graph, n *depgraph.Noun, vars map[string]co continue } + // If we're ignoring self-references, then don't add that + // dependency. + if removeSelf && n == target { + continue + } + // Build the dependency dep := &depgraph.Dependency{ Name: rv.ResourceId(), diff --git a/terraform/plan.go b/terraform/plan.go index 739561f1f..05df1672f 100644 --- a/terraform/plan.go +++ b/terraform/plan.go @@ -14,6 +14,7 @@ import ( func init() { gob.Register(make([]interface{}, 0)) gob.Register(make([]map[string]interface{}, 0)) + gob.Register(make(map[string]string)) } // PlanOpts are the options used to generate an execution plan for diff --git a/terraform/state.go b/terraform/state.go index bd2767d5c..c9812abdb 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -42,6 +42,9 @@ func (s *State) deepcopy() *State { for k, v := range s.Resources { result.Resources[k] = v } + for k, v := range s.Tainted { + result.Tainted[k] = v + } } return result diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index d9f9027fd..05be1c85a 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -139,6 +139,13 @@ aws_instance.foo: type = aws_instance ` +const testTerraformApplyProvisionerResourceRefStr = ` +aws_instance.bar: + ID = foo + num = 2 + type = aws_instance +` + const testTerraformApplyDestroyStr = ` ` diff --git a/terraform/test-fixtures/apply-provisioner-resource-ref/main.tf b/terraform/test-fixtures/apply-provisioner-resource-ref/main.tf new file mode 100644 index 000000000..056a6304a --- /dev/null +++ b/terraform/test-fixtures/apply-provisioner-resource-ref/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "bar" { + num = "2" + + provisioner "shell" { + foo = "${aws_instance.bar.num}" + } +} diff --git a/website/source/docs/commands/agent.html.markdown b/website/source/docs/commands/agent.html.markdown deleted file mode 100644 index 5c262ad26..000000000 --- a/website/source/docs/commands/agent.html.markdown +++ /dev/null @@ -1,16 +0,0 @@ ---- -layout: "docs" -page_title: "Commands: Agent" -sidebar_current: "docs-commands-agent" ---- - -# Terraform Agent - -The `terraform agent` command is the heart of Terraform: it runs the agent that -performs the important task of maintaining membership information, -running checks, announcing services, handling queries, etc. - -Due to the power and flexibility of this command, the Terraform agent -is documented in its own section. See the [Terraform Agent](/docs/agent/basics.html) -section for more information on how to use this command and the -options it has. diff --git a/website/source/docs/commands/index.html.markdown b/website/source/docs/commands/index.html.markdown new file mode 100644 index 000000000..eeacfa5d9 --- /dev/null +++ b/website/source/docs/commands/index.html.markdown @@ -0,0 +1,50 @@ +--- +layout: "docs" +page_title: "Commands" +sidebar_current: "docs-commands" +--- + +# Terraform Commands (CLI) + +Terraform is controlled via a very easy to use command-line interface (CLI). +Terraform is only a single command-line application: terraform. This application +then takes a subcommand such as "apply" or "plan". The complete list of subcommands +is in the navigation to the left. + +The terraform CLI is a well-behaved command line application. In erroneous cases, +a non-zero exit status will be returned. It also responds to -h and --help as you'd +most likely expect. + +To view a list of the available commands at any time, just run terraform with no arguments: + +``` +$ terraform +usage: terraform [--version] [--help] [] + +Available commands are: + apply Builds or changes infrastructure + graph Create a visual graph of Terraform resources + output Read an output from a state file + plan Generate and show an execution plan + refresh Update local state file against real resources + show Inspect Terraform state or plan + version Prints the Terraform version +``` + +To get help for any specific command, pass the -h flag to the relevant subcommand. For example, +to see help about the members subcommand: + +``` +$ terraform graph -h +Usage: terraform graph [options] PATH + + Outputs the visual graph of Terraform resources. If the path given is + the path to a configuration, the dependency graph of the resources are + shown. If the path is a plan file, then the dependency graph of the + plan itself is shown. + + The graph is outputted in DOT format. The typical program that can + read this format is GraphViz, but many web services are also available + to read this format. +``` + diff --git a/website/source/docs/commands/refresh.html.markdown b/website/source/docs/commands/refresh.html.markdown new file mode 100644 index 000000000..06f42f7fd --- /dev/null +++ b/website/source/docs/commands/refresh.html.markdown @@ -0,0 +1,40 @@ +--- +layout: "docs" +page_title: "Command: refresh" +sidebar_current: "docs-commands-refresh" +--- + +# Command: refresh + +The `terraform refresh` command is used to reconcile the state Terraform +knows about (via it's state file) with the real-world infrastructure. +The can be used to detect any drift from the last-known state, and to +update the state file. + +This does not modify infrastructure, but does modify the state file. +If the state is changed, this may cause changes to occur during the next +plan or apply. + +## Usage + +Usage: `terraform refresh [options] [dir]` + +By default, `refresh` requires no flags and looks in the current directory +for the configuration and state file to refresh. + +The command-line flags are all optional. The list of available flags are: + +* `-no-color` - Disables output with coloring + +* `-state=path` - Path to read and write the state file to. Defaults to "terraform.tfstate". + +* `-state-out=path` - Path to write updated state file. By default, the + `-state` path will be used. + +* `-var 'foo=bar'` - Set a variable in the Terraform configuration. This + flag can be set multiple times. + +* `-var-file=foo` - Set variables in the Terraform configuration from + a file. If "terraform.tfvars" is present, it will be automatically + loaded if this flag is not specified. + diff --git a/website/source/docs/commands/show.html.markdown b/website/source/docs/commands/show.html.markdown new file mode 100644 index 000000000..9888884cb --- /dev/null +++ b/website/source/docs/commands/show.html.markdown @@ -0,0 +1,24 @@ +--- +layout: "docs" +page_title: "Command: show" +sidebar_current: "docs-commands-show" +--- + +# Command: show + +The `terraform show` command is used to provide human-readable output +from a state or plan file. This can be used to inspect a plan to ensure +that the planned operations are expected, or to inspect the current state +as terraform sees it. + +## Usage + +Usage: `terraform show [options] ` + +You must call `show` with a path to either a Terraform state file or plan +file. + +The command-line flags are all optional. The list of available flags are: + +* `-no-color` - Disables output with coloring + diff --git a/website/source/docs/providers/aws/r/autoscale.html.markdown b/website/source/docs/providers/aws/r/autoscale.html.markdown new file mode 100644 index 000000000..e1e2481d5 --- /dev/null +++ b/website/source/docs/providers/aws/r/autoscale.html.markdown @@ -0,0 +1,57 @@ +--- +layout: "aws" +page_title: "AWS: aws_autoscaling_group" +sidebar_current: "docs-aws-resource-autoscale" +--- + +# aws\_autoscaling\_group + +Provides an AutoScaling Group resource. + +## Example Usage + +``` +resource "aws_autoscaling_group" "bar" { + availability_zones = ["us-east-1a"] + name = "foobar3-terraform-test" + max_size = 5 + min_size = 2 + health_check_grace_period = 300 + health_check_type = "ELB" + desired_capicity = 4 + force_delete = true + launch_configuration = "${aws_launch_configuration.foobar.name}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the auto scale group. +* `max_size` - (Required) The maximum size of the auto scale group. +* `min_size` - (Required) The minimum size of the auto scale group. +* `availability_zones` - (Required) A list of AZs to launch resources in. +* `launch_configuration` - (Required) The ID of the launch configuration to use. +* `health_check_grace_period` - (Optional) Time after instance comes into service before checking health. +* `health_check_type` - (Optional) "EC2" or "ELB". Controls how health checking is done. +* `desired_capicity` - (Optional) The number of Amazon EC2 instances that should be running in the group. +* `force_delete` - (Optional) Allows deleting the autoscaling group without waiting + for all instances in the pool to terminate. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The autoscaling group name. +* `availability_zones` - The availability zones of the autoscale group. +* `min_size` - The minimum size of the autoscale group +* `max_size` - The maximum size of the autoscale group +* `default_cooldown` - Time between a scaling activity and the succeeding scaling activity. +* `name` - The name of the autoscale group +* `health_check_grace_period` - Time after instance comes into service before checking health. +* `health_check_type` - "EC2" or "ELB". Controls how health checking is done. +* `desired_capicity` -The number of Amazon EC2 instances that should be running in the group. +* `launch_configuration` - The launch configuration of the autoscale group +* `vpc_zone_identifier` - The VPC zone identifier + diff --git a/website/source/docs/providers/aws/r/db_instance.html.markdown b/website/source/docs/providers/aws/r/db_instance.html.markdown new file mode 100644 index 000000000..e5de48776 --- /dev/null +++ b/website/source/docs/providers/aws/r/db_instance.html.markdown @@ -0,0 +1,71 @@ +--- +layout: "aws" +page_title: "AWS: aws_db_instance" +sidebar_current: "docs-aws-resource-db-instance" +--- + +# aws\_db\_instance + +Provides an RDS instance resource. + +## Example Usage + +``` +resource "aws_db_instance" "default" { + identifier = "mydb-rds" + allocated_storage = 10 + engine = "mysql" + engine_version = "5.6.17" + instance_class = "db.t1.micro" + name = "mydb" + username = "foo" + password = "bar" + security_group_names = ["${aws_db_security_group.bar.name}"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `allocated_storage` - (Required) The allocated storage in gigabytes. +* `engine` - (Required) The database engine to use. +* `engine_version` - (Required) The engine version to use. +* `identifier` - (Required) The name of the RDS instance +* `instance_class` - (Required) The instance type of the RDS instance. +* `name` - (Required) The DB name to create. +* `password` - (Required) Password for the master DB user. +* `username` - (Required) Username for the master DB user. +* `availability_zone` - (Optional) The AZ for the RDS instance. +* `backup_retention_period` - (Optional) The days to retain backups for. +* `backup_window` - (Optional) The backup window. +* `iops` - (Optional) The amount of provisioned IOPS +* `maintenance_window` - (Optional) The window to perform maintanence in. +* `multi_az` - (Optional) Specifies if the RDS instance is multi-AZ +* `port` - (Optional) The port on which the DB accepts connetions. +* `publicly_accessible` - (Optional) Bool to control if instance is publically accessible. +* `vpc_security_group_ids` - (Optional) List of VPC security groups to associate. +* `skip_final_snapshot` - (Optional) Enables skipping the final snapshot on deletion. +* `security_group_names` - (Optional) List of DB Security Groups to associate. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The RDS instance ID. +* `address` - The address of the RDS instance. +* `allocated_storage` - The amount of allocated storage +* `availability_zone` - The availability zone of the instance +* `backup_retention_period` - The backup retention period +* `backup_window` - The backup window +* `endpoint` - The connection endpoint +* `engine` - The database engine +* `engine_version` - The database engine version +* `instance_class`- The RDS instance class +* `maintenance_window` - The instance maintenance window +* `multi_az` - If the RDS instance is multi AZ enabled +* `name` - The database name +* `port` - The database port +* `status` - The RDS instance status +* `username` - The master username for the database + diff --git a/website/source/docs/providers/aws/r/db_security_group.html.markdown b/website/source/docs/providers/aws/r/db_security_group.html.markdown new file mode 100644 index 000000000..61fde66c9 --- /dev/null +++ b/website/source/docs/providers/aws/r/db_security_group.html.markdown @@ -0,0 +1,43 @@ +--- +layout: "aws" +page_title: "AWS: aws_db_security_group" +sidebar_current: "docs-aws-resource-db-security-group" +--- + +# aws\_db\_security\_group + +Provides an RDS security group resource. + +## Example Usage + +``` +resource "aws_db_security_group" "default" { + name = "RDS default security group" + ingress { + cidr = "10.0.0.1/24" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the DB security group. +* `description` - (Required) The description of the DB security group. +* `ingress` - (Optional) A list of ingress rules. + +Ingress blocks support the following: + +* `cidr` - The CIDR block to accept +* `security_group_name` - The name of the security group to authorize +* `security_group_id` - The ID of the security group to authorize +* `security_group_owner_id` - The owner Id of the security group provided + by `security_group_name`. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The db security group ID. + diff --git a/website/source/docs/providers/do/index.html.markdown b/website/source/docs/providers/do/index.html.markdown new file mode 100644 index 000000000..c074d6ac2 --- /dev/null +++ b/website/source/docs/providers/do/index.html.markdown @@ -0,0 +1,34 @@ +--- +layout: "digitalocean" +page_title: "Provider: DigitalOcean" +sidebar_current: "docs-do-index" +--- + +# DigitalOcean Provider + +The DigitalOcean (DO) provider is used to interact with the +resources supported by DigitalOcean. The provider needs to be configured +with the proper credentials before it can be used. + +Use the navigation to the left to read about the available resources. + +## Example Usage + +``` +# Configure the DigitalOcean Provider +provider "digitalocean" { + token = "${var.do_token}" +} + +# Create a web server +resource "digitalocean_droplet" "web" { + ... +} +``` + +## Argument Reference + +The following arguments are supported: + +* `token` - (Required) This is the DO API token. + diff --git a/website/source/docs/providers/do/r/droplet.html.markdown b/website/source/docs/providers/do/r/droplet.html.markdown new file mode 100644 index 000000000..6a913d71e --- /dev/null +++ b/website/source/docs/providers/do/r/droplet.html.markdown @@ -0,0 +1,53 @@ +--- +layout: "digitalocean" +page_title: "DigitalOcean: digitalocean_droplet" +sidebar_current: "docs-do-resource-droplet" +--- + +# digitalocean\_droplet + +Provides a DigitalOcean droplet resource. This can be used to create, +modify, and delete droplets. Droplets also support +[provisioning](/docs/provisioners/index.html). + +## Example Usage + +``` +# Create a new Web droplet in the nyc2 region +resource "digitalocean_droplet" "web" { + image = "ubuntu1404" + name = "web-1" + region = "nyc2" + size = "512mb" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `image` - (Required) The droplet image ID or slug. +* `name` - (Required) The droplet name +* `region` - (Required) The region to start in +* `size` - (Required) The instance size to start +* `backups` - (Optional) Boolean controling if backups are made. +* `ipv6` - (Optional) Boolean controling if IPv6 is enabled. +* `private_networking` - (Optional) Boolean controling if private networks are enabled. +* `ssh_keys` - (Optional) A list of SSH IDs or fingerprints to enable. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the droplet +* `name`- The name of the droplet +* `region` - The region of the droplet +* `image` - The image of the droplet +* `ipv6` - Is IPv6 enabled +* `ipv6_address` - The IPv6 address +* `ipv4_address` - The IPv4 address +* `locked` - Is the Droplet locked +* `private_networking` - Is private networking enabled +* `size` - The instance size +* `status` - The status of the droplet + diff --git a/website/source/docs/providers/heroku/index.html.markdown b/website/source/docs/providers/heroku/index.html.markdown new file mode 100644 index 000000000..f1b451729 --- /dev/null +++ b/website/source/docs/providers/heroku/index.html.markdown @@ -0,0 +1,36 @@ +--- +layout: "heroku" +page_title: "Provider: Heroku" +sidebar_current: "docs-heroku-index" +--- + +# Heroku Provider + +The Heroku provider is used to interact with the +resources supported by Heroku. The provider needs to be configured +with the proper credentials before it can be used. + +Use the navigation to the left to read about the available resources. + +## Example Usage + +``` +# Configure the Heroku provider +provider "heroku" { + email = "ops@company.com" + api_key = "${var.heroku_api_key}" +} + +# Create a new applicaiton +resource "heroku_app" "default" { + ... +} +``` + +## Argument Reference + +The following arguments are supported: + +* `api_key` - (Required) Heroku API token +* `email` - (Required) Email to be notified by Heroku + diff --git a/website/source/docs/providers/heroku/r/addon.html.markdown b/website/source/docs/providers/heroku/r/addon.html.markdown new file mode 100644 index 000000000..eb7ced786 --- /dev/null +++ b/website/source/docs/providers/heroku/r/addon.html.markdown @@ -0,0 +1,46 @@ +--- +layout: "heroku" +page_title: "Heroku: heroku_addon" +sidebar_current: "docs-heroku-resource-addon" +--- + +# heroku\_addon + +Provides a Heroku Add-On resource. These can be attach +services to a Heroku app. + +## Example Usage + +``` +# Create a new heroku app +resource "heroku_app" "default" { + name = "test-app" +} + +# Add a web-hook addon for the app +resource "heroku_addon" "webhook" { + app = "${heroku_app.default.name}" + plan = "deployhooks:http" + config { + url = "http://google.com" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `app` - (Required) The Heroku app to add to. +* `plan` - (Required) The addon to add. +* `config` - (Optional) Optional plan configuration. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the add-on +* `name` - The add-on name +* `plan` - The plan name +* `provider_id` - The ID of the plan provider + diff --git a/website/source/docs/providers/heroku/r/app.html.markdown b/website/source/docs/providers/heroku/r/app.html.markdown new file mode 100644 index 000000000..377e75460 --- /dev/null +++ b/website/source/docs/providers/heroku/r/app.html.markdown @@ -0,0 +1,41 @@ +--- +layout: "heroku" +page_title: "Heroku: heroku_app" +sidebar_current: "docs-heroku-resource-app" +--- + +# heroku\_app + +Provides a Heroku App resource. This can be used to +create and manage applications on Heroku. + +## Example Usage + +``` +# Create a new heroku app +resource "heroku_app" "default" { + name = "my-cool-app" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Optional) The name of the Heroku app +* `region` - (Optional) The region of the Heroku app +* `stack` - (Optional) The stack for the Heroku app +* `config_vars` - (Optional) Configuration variables for the app + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the app +* `name` - The name of the app +* `stack` - The stack of the app +* `region` - The region of the app +* `git_url` - The Git URL for the app +* `web_url` - The Web URL for the app +* `heroku_hostname` - The Heroku URL for the app + diff --git a/website/source/docs/providers/heroku/r/domain.html.markdown b/website/source/docs/providers/heroku/r/domain.html.markdown new file mode 100644 index 000000000..b02b4b591 --- /dev/null +++ b/website/source/docs/providers/heroku/r/domain.html.markdown @@ -0,0 +1,41 @@ +--- +layout: "heroku" +page_title: "Heroku: heroku_domain" +sidebar_current: "docs-heroku-resource-domain" +--- + +# heroku\_domain + +Provides a Heroku App resource. This can be used to +create and manage applications on Heroku. + +## Example Usage + +``` +# Create a new heroku app +resource "heroku_app" "default" { + name = "test-app" +} + +# Associate a custom domain +resource "heroku_domain" "default" { + app = "${heroku_app.default.name}" + hostname = "terraform.example.com" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `hostname` - (Required) The hostname to serve requests from. +* `app` - (Required) The Heroku app to link to. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the of the domain record +* `hostname` - The hostname traffic will be served as +* `cname` - The cname traffic should route to. + diff --git a/website/source/docs/provisioners/remote-exec.html.markdown b/website/source/docs/provisioners/remote-exec.html.markdown index 0b6123115..b677fe32c 100644 --- a/website/source/docs/provisioners/remote-exec.html.markdown +++ b/website/source/docs/provisioners/remote-exec.html.markdown @@ -22,7 +22,7 @@ resource "aws_instance" "web" { provisioner "remote-exec" { inline = [ "puppet apply", - "consul join ${aws_instance.web.private_ip", + "consul join ${aws_instance.web.private_ip}", ] } } diff --git a/website/source/intro/getting-started/build.html.md b/website/source/intro/getting-started/build.html.md index e0a19274b..646e83654 100644 --- a/website/source/intro/getting-started/build.html.md +++ b/website/source/intro/getting-started/build.html.md @@ -194,6 +194,19 @@ a lot more metadata about it. This metadata can actually be referenced for other resources or outputs, which will be covered later in the getting started guide. +## Provisioning + +The EC2 instance we launched at this point is based on the AMI +given, but has no additional software installed. If you're running +an image-based infrastructure (perhaps creating images with +[Packer](http://www.packer.io)), then this is all you need. + +However, many infrastructures still require some sort of initialization +or software provisioning step. Terraform supports +provisioners, +which we'll cover a little bit later in the getting started guide, +in order to do this. + ## Next Congratulations! You've built your first infrastructure with Terraform. diff --git a/website/source/intro/getting-started/dependencies.html.md b/website/source/intro/getting-started/dependencies.html.md new file mode 100644 index 000000000..5d7c004f4 --- /dev/null +++ b/website/source/intro/getting-started/dependencies.html.md @@ -0,0 +1,165 @@ +--- +layout: "intro" +page_title: "Resource Dependencies" +sidebar_current: "gettingstarted-deps" +--- + +# Resource Dependencies + +In this page, we're going to introduce resource dependencies, +where we'll not only see a configuration with multiple resources +for the first time, but also scenarios where resource parameters +use information from other resources. + +Up to this point, our example has only contained a single resource. +Real infrastructure has a diverse set of resources and resource +types. Terraform configurations can contain multiple resources, +multiple resource types, and these types can even span multiple +providers. + +On this page, we'll show a basic example of multiple resources +and how to reference the attributes of other resources to configure +subsequent resources. + +## Assigning an Elastic IP + +We'll improve our configuration by assigning an elastic IP to +the EC2 instance we're managing. Modify your `example.tf` and +add the following: + +``` +resource "aws_eip" "ip" { + instance = "${aws_instance.example.id}" +} +``` + +This should look familiar from the earlier example of adding +an EC2 instance resource, except this time we're building +an "aws\_eip" resource type. This resource type allocates +and associates an +[elastic IP](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html) +to an EC2 instance. + +The only parameter for +[aws\_eip](/docs/providers/aws/r/eip.html) is "instance" which +is the EC2 instance to assign the IP to. For this value, we +use an interpolation to use an attribute from the EC2 instance +we managed earlier. + +The syntax for this interpolation should be straightforward: +it requests the "id" attribute from the "aws\_instance.example" +resource. + +## Plan and Execute + +Run `terraform plan` to view the execution plan. The output +will look something like the following: + +``` +$ terraform plan +... + ++ aws_eip.ip + instance: "" => "${aws_instance.example.id}" + private_ip: "" => "" + public_ip: "" => "" + ++ aws_instance.example + ami: "" => "ami-aa7ab6c2" + availability_zone: "" => "" + instance_type: "" => "t1.micro" + key_name: "" => "" + private_dns: "" => "" + private_ip: "" => "" + public_dns: "" => "" + public_ip: "" => "" + security_groups: "" => "" + subnet_id: "" => "" +``` + +Terraform will create two resources: the instance and the elastic +IP. In the "instance" value for the "aws\_eip", you can see the +raw interpolation is still present. This is because this variable +won't be known until the "aws\_instance" is created. It will be +replaced at apply-time. + +Next, run `terraform apply`. The output will look similar to the +following: + +``` +aws_instance.example: Creating... + ami: "" => "ami-aa7ab6c2" + instance_type: "" => "t1.micro" +aws_eip.ip: Creating... + instance: "" => "i-0e737b25" + +Apply complete! Resources: 2 added, 0 changed, 0 destroyed. +``` + +It is clearer to see from actually running Terraform, but +Terraform creates the EC2 instance before the elastic IP +address. Due to the interpolation earlier where the elastic +IP requires the ID of the EC2 instance, Terraform is able +to infer a dependency, and knows to create the instance +first. + +## Implicit and Explicit Dependencies + +Most dependencies in Terraform are implicit: Terraform is able +to infer dependencies based on usage of attributes of other +resources. + +Using this information, Terraform builds a graph of resources. +This tells Terraform not only what order to create resources, +but also what resources can be created in parallel. In our example, +since the IP address depended on the EC2 instance, they could +not be created in parallel. + +Implicit dependencies work well and are usually all you ever need. +However, you can also specify explicit dependencies with the +`depends_on` parameter which is available on any resource. For example, +we could modify the "aws\_eip" resource to the following, which +effectively does the same thing and is redundant: + +``` +resource "aws_eip" "ip" { + instance = "${aws_instance.example.id}" + depends_on = ["aws_instance.example"] +} +``` + +If you're ever unsure about the dependency chain that Terraform +is creating, you can use the `terraform graph` command to view +the graph. This command outputs a dot-formatted graph which can be +viewed with +[Graphviz](http://www.graphviz.org/). + +## Non-Dependent Resources + +We can now augment the configuration with another EC2 instance. +Because this doesn't rely on any other resource, it can be +created in parallel to everything else. + +``` +resource "aws_instance" "another" { + ami = "ami-aa7ab6c2" + instance_type = "t1.micro" +} +``` + +You can view the graph with `terraform graph` to see that +nothing depends on this and that it will likely be created +in parallel. + +Before moving on, remove this resource from your configuration +and `terraform apply` again to destroy it. We won't use the +second instance anymore in the getting started guide. + +## Next + +In this page you were introduced to both multiple resources +as well as basic resource dependencies and resource attribute +interpolation. + +Moving on, we'll use provisioners to do some basic bootstrapping +of our launched instance. diff --git a/website/source/intro/getting-started/destroy.html.md b/website/source/intro/getting-started/destroy.html.md index 1f4ff9a70..6db76aec7 100644 --- a/website/source/intro/getting-started/destroy.html.md +++ b/website/source/intro/getting-started/destroy.html.md @@ -18,35 +18,37 @@ destroying is a useful action. ## Plan -While our infrastructure is simple, viewing the execution plan -of a destroy can be useful to make sure that it is destroying -only the resources you expect. - -To ask Terraform to create an execution plan to destroy all -infrastructure, run the plan command with the `-destroy` flag. +For Terraform to destroy our infrastructure, we need to ask +Terraform to generate a destroy execution plan. This is a special +kind of execution plan that only destroys all Terraform-managed +infrastructure, and doesn't create or update any components. ``` -$ terraform plan -destroy +$ terraform plan -destroy -out=terraform.tfplan ... - aws_instance.example ``` -The output says that "aws\_instance.example" will be deleted. +The plan command is given two new flags. -The `-destroy` flag lets you destroy infrastructure without -modifying the configuration. You can also destroy infrastructure -by simply commenting out or deleting the contents of your -configuration, but usually you just want to destroy an instance -of your infrastructure rather than permanently deleting your -configuration as well. The `-destroy` flag is for this case. +The first flag, `-destroy` tells Terraform to create an execution +plan to destroy the infrastructure. You can see in the output that +our one EC2 instance will be destroyed. + +The second flag, `-out` tells Terraform to save the execution plan +to a file. We haven't seen this before, but it isn't limited to +only destroys. Any plan can be saved to a file. Terraform can then +apply a plan, ensuring that only exactly the plan you saw is executed. +For destroys, you must save into a plan, since there is no way to +tell `apply` to destroy otherwise. ## Apply Let's apply the destroy: ``` -$ terraform apply -destroy +$ terraform apply terraform.tfplan aws_instance.example: Destroying... Apply complete! Resources: 0 added, 0 changed, 1 destroyed. @@ -57,6 +59,9 @@ Apply complete! Resources: 0 added, 0 changed, 1 destroyed. Done. Terraform destroyed our one instance, and if you run a `terraform show`, you'll see that the state file is now empty. +For this command, we gave an argument to `apply` for the first +time. You can give apply a specific plan to execute. + ## Next You now know how to create, modify, and destroy infrastructure. diff --git a/website/source/intro/getting-started/next-steps.html.markdown b/website/source/intro/getting-started/next-steps.html.markdown index cabed244f..bbbe55df4 100644 --- a/website/source/intro/getting-started/next-steps.html.markdown +++ b/website/source/intro/getting-started/next-steps.html.markdown @@ -6,24 +6,16 @@ sidebar_current: "gettingstarted-nextsteps" # Next Steps -That concludes the getting started guide for Terraform. Hopefully you're able to -see that while Terraform is simple to use, it has a powerful set of features. -We've covered the basics for all of these features in this guide. +That concludes the getting started guide for Terraform. Hopefully +you're now able to not only see what Terraform is useful for, but +you're also able to put this knowledge to use to improve building +your own infrastructure. -Terraform is designed to be friendly to both the DevOps community and -application developers, making it perfect for modern, elastic infrastructures. +We've covered the basics for all of these features in this guide. As a next step, the following resources are available: -* [Documentation](/docs/index.html) - The documentation is an in-depth reference - guide to all the features of Terraform, including technical details about the - internals of how Terraform operates. - -* [Guides](/docs/guides/index.html) - This section provides various getting - started guides with Terraform, including how to bootstrap a new datacenter. - -* [Examples](https://github.com/hashicorp/terraform/tree/master/demo) - - The work-in-progress examples folder within the GitHub - repository for Terraform contains functional examples of various use cases - of Terraform to help you get started with exactly what you need. +* [Documentation](/docs/index.html) - The documentation is an in-depth + reference guide to all the features of Terraform, including + technical details about the internals of how Terraform operates. diff --git a/website/source/intro/getting-started/outputs.html.md b/website/source/intro/getting-started/outputs.html.md new file mode 100644 index 000000000..b0883bb8d --- /dev/null +++ b/website/source/intro/getting-started/outputs.html.md @@ -0,0 +1,78 @@ +--- +layout: "intro" +page_title: "Output Variables" +sidebar_current: "gettingstarted-outputs" +--- + +# Output Variables + +In the previous section, we introduced input variables as a way +to parameterize Terraform configurations. In this page, we +introduce output variables as a way to organize data to be +easily queried and shown back to the Terraform user. + +When building potentially complex infrastructure, Terraform +stores hundreds or thousands of attribute values for all your +resources. But as a user of Terraform, you may only be interested +in a few values of importance, such as a load balancer IP, +VPN address, etc. + +Outputs are a way to tell Terraform what data is important. +This data is outputted when `apply` is called, and can be +queried using the `terraform output` command. + +## Defining Outputs + +Let's define an output to show us the public IP address of the +elastic IP address that we create. Add this to any of your +`*.tf` files: + +``` +output "ip" { + value = "${aws_eip.ip.public_ip}" +} +``` + +This defines an output variables named "ip". The `value` field +specifies what the value will be, and almost always contains +one or more interpolations, since the output data is typically +dynamic in some form. In this case, we're outputting the +`public_ip` attribute of the elastic IP address. + +Multiple `output` blocks can be defined to specify multiple +output variables. + +## Viewing Outputs + +Run `terraform apply` to populate the output. This only needs +to be done once after the output is defined. The apply output +should change slightly. At the end you should see this: + +``` +$ terraform apply +... + +Apply complete! Resources: 0 added, 0 changed, 0 destroyed. + +Outputs: + + ip = 50.17.232.209 +``` + +`apply` highlights the outputs. You can also query the outputs +after apply-time using `terraform output`: + +``` +$ terraform output ip +50.17.232.209 +``` + +This command is useful for scripts to extract outputs. + +## Next + +You now know how to parameterize configurations with input +variables, and extract important data using output variables. + +Next, we're going to use provisioners to install some software +on the instances created on top of the base AMI used. diff --git a/website/source/intro/getting-started/provision.html.md b/website/source/intro/getting-started/provision.html.md new file mode 100644 index 000000000..563609655 --- /dev/null +++ b/website/source/intro/getting-started/provision.html.md @@ -0,0 +1,110 @@ +--- +layout: "intro" +page_title: "Provision" +sidebar_current: "gettingstarted-provision" +--- + +# Provision + +You're now able to create and modify infrastructure. This page +introduces how to use provisioners to run basic shell scripts on +instances when they're created. + +If you're using an image-based infrastructure (perhaps with images +created with [Packer](http://www.packer.io)), then what you've +learned so far is good enough. But if you need to do some initial +setup on your instances, provisioners let you upload files, +run shell scripts, etc. + +## Defining a Provisioner + +To define a provisioner, modify the resource block defining the +"example" EC2 instance to look like the following: + +``` +resource "aws_instance" "example" { + ami = "ami-aa7ab6c2" + instance_type = "t1.micro" + + provisioner "local-exec" { + command = "echo ${aws_instance.example.public_ip} > file.txt" + } +} +``` + +This adds a `provision` block within the `resource` block. Multiple +`provision` blocks can be added to define multiple provisoining steps. +Terraform supports +[multiple provisioners](/docs/provisioners/index.html), +but for this example we use the "local-exec" provisioner. + +The "local-exec" provisioner executes a command locally on the machine +running Terraform. We're using this provisioner versus the others so +we don't have to worry about specifying any +[connection info](/docs/provisioners/connection.html) right now. + +## Running Provisioners + +Provisioners are run only when a resource is _created_. They +are not a replacement for configuration management and changing +the software of an already-running server, and are instead just +meant as a way to bootstrap a server. For configuration management, +you should use Terraform provisioning to bootstrap a real configuration +management solution. + +Make sure that your infrastructure is +[destroyed](/intro/getting-started/destroy.html) if it isn't already, +then run `apply`: + +``` +$ terraform apply +aws_instance.example: Creating... + ami: "" => "ami-aa7ab6c2" + instance_type: "" => "t1.micro" +aws_eip.ip: Creating... + instance: "" => "i-213f350a" + +Apply complete! Resources: 2 added, 0 changed, 0 destroyed. +``` + +Terraform currently doesn't output anything to indicate the provisioners +have run. This is going to be fixed soon. However, we can verify +everything worked by looking at the "file.txt" file: + +``` +$ cat file.txt +54.192.26.128 +``` + +It contains the IP, just ask we asked! + +## Failed Provisioners and Tainted Resources + +If a resource successfully creates but fails during provision, +Terraform will error and mark the resource as "tainted." A +resource that is tainted has been physically created, but can't +be considered safe to use since provisioning failed. + +When you generate your next execution plan, Terraform will remove +any tainted resources and create new resources, attempting to +provision again. It does not attempt to restart provisioning on the +same resource because it isn't guaranteed to be safe. + +Terraform does not automatically roll back and destroy the resource +during the apply when the failure happens, because that would go +against the execution plan: the execution plan would've said a +resource will be created, but does not say it will ever be deleted. +But if you create an execution plan with a tainted resource, the +plan will clearly state that the resource will be destroyed because +it is tainted. + +## Next + +Provisioning is important for being able to bootstrap instances. +As another reminder, it is not a replacement for configuration +management. It is meant to simply bootstrap machines. If you use +configuration management, you should use the provisioning as a way +to bootstrap the configuration management utility. + +In the next section, we start looking at variables as a way to +better parameterize our configurations. diff --git a/website/source/intro/getting-started/variables.html.md b/website/source/intro/getting-started/variables.html.md new file mode 100644 index 000000000..882e66e7e --- /dev/null +++ b/website/source/intro/getting-started/variables.html.md @@ -0,0 +1,141 @@ +--- +layout: "intro" +page_title: "Input Variables" +sidebar_current: "gettingstarted-variables" +--- + +# Input Variables + +You now have enough Terraform knowledge to create useful +configurations, but we're still hardcoding access keys, +AMIs, etc. To become truly shareable and commitable to version +control, we need to parameterize the configurations. This page +introduces input variables as a way to do this. + +## Defining Variables + +Let's first extract our access key, secret key, and region +into a few variables. Create another file `variables.tf` with +the following contents. Note that the file can be named anything, +since Terraform loads all files ending in `.tf` in a directory. + +``` +variable "access_key" {} +variable "secret_key" {} +variable "region" { + default = "us-east-1" +} +``` + +This defines three variables within your Terraform configuration. +The first two have empty blocks `{}`. The third sets a default. If +a default value is set, the variable is optional. Otherwise, the +variable is required. If you run `terraform plan` now, Terraform will +error since the required variables are not set. + +## Using Variables in Configuration + +Next, replace the AWS provider configuration with the following: + +``` +provider "aws" { + access_key = "${var.access_key}" + secret_key = "${var.secret_key}" + region = "${var.region}" +} +``` + +This uses more interpolations, this time prefixed with `var.`. This +tells Terraform that you're accessing variables. This configures +the AWS provider with the given variables. + +## Assigning Variables + +There are two ways to assign variables. + +First, you can set it directly on the command-line with the +`-var` flag. Any command in Terraform that inspects the configuration +accepts this flag, such as `apply`, `plan`, and `refresh`: + +``` +$ terraform plan \ + -var 'access_key=foo' \ + -var 'secret_key=bar' +... +``` + +Second, you can create a file and assign variables directly. Create +a file named "terraform.tfvars" with the following contents: + +``` +access_key = "foo" +secret_key = "bar" +``` + +If a "terraform.tfvars" file is present, Terraform automatically loads +it to populate variables. If the file is named something else, you can +use the `-var-file` flag directly to specify a file. + +We recommend using the "terraform.tfvars" file, and ignoring it from +version control. + +## Mappings + +We've replaced our sensitive strings with variables, but we still +are hardcoding AMIs. Unfortunately, AMIs are specific to the region +that is in use. One option is to just ask the user to input the proper +AMI for the region, but Terraform can do better than that with +_mappings_. + +Mappings are a way to create variables that are lookup tables. An example +will show this best. Let's extract our AMIs into a mapping and add +support for the "us-west-2" region as well: + +``` +variable "amis" { + default = { + "us-east-1": "ami-aa7ab6c2", + "us-west-2": "ami-23f78e13", + } +} +``` + +A variable becomes a mapping when it has a default value that is a +map like above. There is no way to create a required map. + +Then, replace the "aws\_instance" with the following: + +``` +resource "aws_instance" "example" { + ami = "${lookup(var.amis, var.region)}" + instance_type = "t1.micro" +} +``` + +This introduces a new type of interpolation: a function call. The +`lookup` function does a dynamic lookup in a map for a key. The +key is `var.region`, which specifies that the value of the region +variables is the key. + +While we don't use it in our example, it is worth nothing that you +can also do a static lookup of a mapping directly with +`${var.amis.us-east-1}`. + +We set defaults, but mappings can also be overridden using the +`-var` and `-var-file` values. For example, if the user wanted to +specify an alternate AMI for us-east-1: + +``` +$ terraform plan -var 'amis.us-east-1=foo' +... +``` + +## Next + +Terraform provides variables for parameterizing your configurations. +Mappings let you build lookup tables in cases where that make sense. +Setting and using variables is uniform throughout your configurations. + +In the next section, we'll take a look at output variables as a +mechanism to expose certain values more prominently to the Terraform +operator. diff --git a/website/source/layouts/_header.erb b/website/source/layouts/_header.erb index fd72bc560..cd3ccce98 100644 --- a/website/source/layouts/_header.erb +++ b/website/source/layouts/_header.erb @@ -60,7 +60,6 @@
  • Intro
  • Docs
  • Community
  • -
  • Demo
  • diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index b1d6397b2..c0c913ee2 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -13,6 +13,18 @@ > Resources