diff --git a/.gitignore b/.gitignore index e7669ef01..5c9083838 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ bin/ config/y.go config/y.output vendor/ +website/.vagrant diff --git a/builtin/bins/provider-dnsimple/main.go b/builtin/bins/provider-dnsimple/main.go new file mode 100644 index 000000000..44860d71b --- /dev/null +++ b/builtin/bins/provider-dnsimple/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/dnsimple" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(new(dnsimple.ResourceProvider)) +} diff --git a/builtin/bins/provider-dnsimple/main_test.go b/builtin/bins/provider-dnsimple/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provider-dnsimple/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/aws/resource_aws_db_security_group.go b/builtin/providers/aws/resource_aws_db_security_group.go index 72be35b4c..a23f6a49a 100644 --- a/builtin/providers/aws/resource_aws_db_security_group.go +++ b/builtin/providers/aws/resource_aws_db_security_group.go @@ -47,7 +47,6 @@ func resource_aws_db_security_group_create( if err != nil { return rs, err } - log.Printf("%#v", rs.Attributes) if _, ok := rs.Attributes["ingress.#"]; ok { ingresses := flatmap.Expand( diff --git a/builtin/providers/aws/resource_provider.go b/builtin/providers/aws/resource_provider.go index dda9cc4c0..92a7b544c 100644 --- a/builtin/providers/aws/resource_provider.go +++ b/builtin/providers/aws/resource_provider.go @@ -2,6 +2,7 @@ package aws import ( "log" + "os" "github.com/hashicorp/terraform/helper/config" "github.com/hashicorp/terraform/helper/multierror" @@ -26,14 +27,30 @@ type ResourceProvider struct { } func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) { - v := &config.Validator{ - Optional: []string{ - "access_key", - "secret_key", - "region", - }, + type param struct { + env string + key string + } + params := []param{ + {"AWS_REGION", "region"}, + {"AWS_ACCESS_KEY", "access_key"}, + {"AWS_SECRET_KEY", "secret_key"}, } + var optional []string + var required []string + for _, p := range params { + if v := os.Getenv(p.env); v != "" { + optional = append(optional, p.key) + } else { + required = append(required, p.key) + } + } + + v := &config.Validator{ + Required: required, + Optional: optional, + } return v.Validate(c) } diff --git a/builtin/providers/dnsimple/config.go b/builtin/providers/dnsimple/config.go new file mode 100644 index 000000000..dfec0f2e1 --- /dev/null +++ b/builtin/providers/dnsimple/config.go @@ -0,0 +1,33 @@ +package dnsimple + +import ( + "log" + "os" + + "github.com/rubyist/go-dnsimple" +) + +type Config struct { + Token string `mapstructure:"token"` + Email string `mapstructure:"email"` +} + +// Client() returns a new client for accessing heroku. +// +func (c *Config) Client() (*dnsimple.DNSimpleClient, error) { + + // If we have env vars set (like in the acc) tests, + // we need to override the values passed in here. + if v := os.Getenv("DNSIMPLE_EMAIL"); v != "" { + c.Email = v + } + if v := os.Getenv("DNSIMPLE_TOKEN"); v != "" { + c.Token = v + } + + client := dnsimple.NewClient(c.Token, c.Email) + + log.Printf("[INFO] DNSimple Client configured for user: %s", client.Email) + + return client, nil +} diff --git a/builtin/providers/dnsimple/resource_dnsimple_record.go b/builtin/providers/dnsimple/resource_dnsimple_record.go new file mode 100644 index 000000000..881b28a71 --- /dev/null +++ b/builtin/providers/dnsimple/resource_dnsimple_record.go @@ -0,0 +1,163 @@ +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" +) + +func resource_dnsimple_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 := dnsimple.Record{ + Name: rs.Attributes["name"], + Content: rs.Attributes["value"], + RecordType: rs.Attributes["type"], + } + + if attr, ok := rs.Attributes["ttl"]; ok { + newRecord.TTL, err = strconv.Atoi(attr) + if err != nil { + return nil, err + } + } + + log.Printf("[DEBUG] record create configuration: %#v", newRecord) + + rec, 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) + + log.Printf("[INFO] record ID: %s", rs.ID) + + return resource_dnsimple_record_update_state(rs, &rec) +} + +func resource_dnsimple_record_update( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + + panic("Cannot update record") + + return nil, nil +} + +func resource_dnsimple_record_destroy( + s *terraform.ResourceState, + meta interface{}) error { + p := meta.(*ResourceProvider) + client := p.client + + log.Printf("[INFO] Deleting record: %s", s.ID) + + rec, err := resource_dnsimple_record_retrieve(s.Attributes["domain"], s.ID, client) + if err != nil { + return err + } + + err = rec.Delete(client) + if err != nil { + return fmt.Errorf("Error deleting record: %s", err) + } + + return nil +} + +func resource_dnsimple_record_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + rec, err := resource_dnsimple_record_retrieve(s.Attributes["app"], s.ID, client) + if err != nil { + return nil, err + } + + return resource_dnsimple_record_update_state(s, rec) +} + +func resource_dnsimple_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.AttrTypeCreate, + "value": diff.AttrTypeUpdate, + "ttl": diff.AttrTypeCreate, + "type": diff.AttrTypeUpdate, + }, + + ComputedAttrs: []string{ + "priority", + "domain_id", + }, + } + + return b.Diff(s, c) +} + +func resource_dnsimple_record_update_state( + s *terraform.ResourceState, + rec *dnsimple.Record) (*terraform.ResourceState, error) { + + 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) + + return s, nil +} + +func resource_dnsimple_record_retrieve(domain string, id string, client *dnsimple.DNSimpleClient) (*dnsimple.Record, error) { + intId, err := strconv.Atoi(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 +} + +func resource_dnsimple_record_validation() *config.Validator { + return &config.Validator{ + Required: []string{ + "domain", + "name", + "value", + "type", + }, + Optional: []string{ + "ttl", + }, + } +} diff --git a/builtin/providers/dnsimple/resource_dnsimple_record_test.go b/builtin/providers/dnsimple/resource_dnsimple_record_test.go new file mode 100644 index 000000000..eb91e7f6d --- /dev/null +++ b/builtin/providers/dnsimple/resource_dnsimple_record_test.go @@ -0,0 +1,115 @@ +package dnsimple + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/rubyist/go-dnsimple" +) + +func TestAccDNSimpleRecord_Basic(t *testing.T) { + var record dnsimple.Record + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDNSimpleRecordDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckDNSimpleRecordConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckDNSimpleRecordExists("dnsimple_record.foobar", &record), + testAccCheckDNSimpleRecordAttributes(&record), + resource.TestCheckResourceAttr( + "dnsimple_record.foobar", "name", "terraform"), + resource.TestCheckResourceAttr( + "dnsimple_record.foobar", "domain", "jack.ly"), + resource.TestCheckResourceAttr( + "dnsimple_record.foobar", "value", "192.168.0.10"), + ), + }, + }, + }) +} + +func testAccCheckDNSimpleRecordDestroy(s *terraform.State) error { + client := testAccProvider.client + + for _, rs := range s.Resources { + if rs.Type != "dnsimple_record" { + continue + } + + intId, err := strconv.Atoi(rs.ID) + if err != nil { + return err + } + + _, err = client.RetrieveRecord(rs.Attributes["domain"], intId) + + if err == nil { + return fmt.Errorf("Record still exists") + } + } + + return nil +} + +func testAccCheckDNSimpleRecordAttributes(record *dnsimple.Record) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if record.Content != "192.168.0.10" { + 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] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.ID == "" { + return fmt.Errorf("No Record ID is set") + } + + client := testAccProvider.client + + intId, err := strconv.Atoi(rs.ID) + if err != nil { + return err + } + + foundRecord, err := client.RetrieveRecord(rs.Attributes["domain"], intId) + + if err != nil { + return err + } + + if strconv.Itoa(foundRecord.Id) != rs.ID { + return fmt.Errorf("Record not found") + } + + *record = foundRecord + + return nil + } +} + +const testAccCheckDNSimpleRecordConfig_basic = ` +resource "dnsimple_record" "foobar" { + domain = "jack.ly" + + name = "terraform" + value = "192.168.0.10" + type = "A" + ttl = 3600 +}` diff --git a/builtin/providers/dnsimple/resource_provider.go b/builtin/providers/dnsimple/resource_provider.go new file mode 100644 index 000000000..478acb470 --- /dev/null +++ b/builtin/providers/dnsimple/resource_provider.go @@ -0,0 +1,68 @@ +package dnsimple + +import ( + "log" + + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/terraform" + "github.com/rubyist/go-dnsimple" +) + +type ResourceProvider struct { + Config Config + + client *dnsimple.DNSimpleClient +} + +func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) { + v := &config.Validator{ + Required: []string{ + "token", + "email", + }, + } + + 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 DNSimple 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() +} diff --git a/builtin/providers/dnsimple/resource_provider_test.go b/builtin/providers/dnsimple/resource_provider_test.go new file mode 100644 index 000000000..63dd0b067 --- /dev/null +++ b/builtin/providers/dnsimple/resource_provider_test.go @@ -0,0 +1,76 @@ +package dnsimple + +import ( + "os" + "reflect" + "testing" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *ResourceProvider + +func init() { + testAccProvider = new(ResourceProvider) + testAccProviders = map[string]terraform.ResourceProvider{ + "dnsimple": testAccProvider, + } +} + +func TestResourceProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = new(ResourceProvider) +} + +func TestResourceProvider_Configure(t *testing.T) { + rp := new(ResourceProvider) + var expectedToken string + var expectedEmail string + + if v := os.Getenv("DNSIMPLE_EMAIL"); v != "" { + expectedEmail = v + } else { + expectedEmail = "foo" + } + + if v := os.Getenv("DNSIMPLE_TOKEN"); v != "" { + expectedToken = v + } else { + expectedToken = "foo" + } + + raw := map[string]interface{}{ + "token": expectedToken, + "email": expectedEmail, + } + + rawConfig, err := config.NewRawConfig(raw) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = rp.Configure(terraform.NewResourceConfig(rawConfig)) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := Config{ + Token: expectedToken, + Email: expectedEmail, + } + + if !reflect.DeepEqual(rp.Config, expected) { + t.Fatalf("bad: %#v", rp.Config) + } +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("DNSIMPLE_EMAIL"); v == "" { + t.Fatal("DNSIMPLE_EMAIL must be set for acceptance tests") + } + + if v := os.Getenv("DNSIMPLE_TOKEN"); v == "" { + t.Fatal("DNSIMPLE_TOKEN must be set for acceptance tests") + } +} diff --git a/builtin/providers/dnsimple/resources.go b/builtin/providers/dnsimple/resources.go new file mode 100644 index 000000000..7cbd5db91 --- /dev/null +++ b/builtin/providers/dnsimple/resources.go @@ -0,0 +1,24 @@ +package dnsimple + +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{ + "dnsimple_record": resource.Resource{ + ConfigValidator: resource_dnsimple_record_validation(), + Create: resource_dnsimple_record_create, + Destroy: resource_dnsimple_record_destroy, + Diff: resource_dnsimple_record_diff, + Update: resource_dnsimple_record_update, + Refresh: resource_dnsimple_record_refresh, + }, + }, + } +} diff --git a/builtin/providers/heroku/resource_heroku_addon.go b/builtin/providers/heroku/resource_heroku_addon.go new file mode 100644 index 000000000..0081b8477 --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_addon.go @@ -0,0 +1,186 @@ +package heroku + +import ( + "fmt" + "log" + + "github.com/bgentry/heroku-go" + "github.com/hashicorp/terraform/flatmap" + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/terraform" +) + +func resource_heroku_addon_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) + + app := rs.Attributes["app"] + plan := rs.Attributes["plan"] + opts := heroku.AddonCreateOpts{} + + if attr, ok := rs.Attributes["config.#"]; ok && attr == "1" { + vs := flatmap.Expand( + rs.Attributes, "config").([]interface{}) + + config := make(map[string]string) + for k, v := range vs[0].(map[string]interface{}) { + config[k] = v.(string) + } + + opts.Config = &config + } + + log.Printf("[DEBUG] Addon create configuration: %#v, %#v, %#v", app, plan, opts) + + a, err := client.AddonCreate(app, plan, &opts) + + if err != nil { + return s, err + } + + rs.ID = a.Id + log.Printf("[INFO] Addon ID: %s", rs.ID) + + addon, err := resource_heroku_addon_retrieve(app, rs.ID, client) + if err != nil { + return rs, err + } + + return resource_heroku_addon_update_state(rs, addon) +} + +func resource_heroku_addon_update( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + rs := s.MergeDiff(d) + + app := rs.Attributes["app"] + + if attr, ok := d.Attributes["plan"]; ok { + ad, err := client.AddonUpdate( + app, rs.ID, + attr.New) + + if err != nil { + return s, err + } + + // Store the new ID + rs.ID = ad.Id + } + + addon, err := resource_heroku_addon_retrieve(app, rs.ID, client) + + 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 +} + +func resource_heroku_addon_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + app, err := resource_heroku_addon_retrieve(s.Attributes["app"], s.ID, client) + if err != nil { + return nil, err + } + + return resource_heroku_addon_update_state(s, app) +} + +func resource_heroku_addon_diff( + s *terraform.ResourceState, + c *terraform.ResourceConfig, + meta interface{}) (*terraform.ResourceDiff, error) { + + b := &diff.ResourceBuilder{ + Attrs: map[string]diff.AttrType{ + "app": diff.AttrTypeCreate, + "plan": diff.AttrTypeUpdate, + "config": diff.AttrTypeCreate, + }, + + ComputedAttrs: []string{ + "provider_id", + "config_vars", + }, + } + + return b.Diff(s, c) +} + +func resource_heroku_addon_update_state( + s *terraform.ResourceState, + addon *heroku.Addon) (*terraform.ResourceState, error) { + + s.Attributes["name"] = addon.Name + s.Attributes["plan"] = addon.Plan.Name + s.Attributes["provider_id"] = addon.ProviderId + + toFlatten := make(map[string]interface{}) + + if len(addon.ConfigVars) > 0 { + toFlatten["config_vars"] = addon.ConfigVars + } + + for k, v := range flatmap.Flatten(toFlatten) { + s.Attributes[k] = v + } + + return s, nil +} + +func resource_heroku_addon_retrieve(app string, id string, client *heroku.Client) (*heroku.Addon, error) { + addon, err := client.AddonInfo(app, id) + + if err != nil { + return nil, fmt.Errorf("Error retrieving addon: %s", err) + } + + return addon, nil +} + +func resource_heroku_addon_validation() *config.Validator { + return &config.Validator{ + Required: []string{ + "app", + "plan", + }, + Optional: []string{ + "config.*", + }, + } +} diff --git a/builtin/providers/heroku/resource_heroku_addon_test.go b/builtin/providers/heroku/resource_heroku_addon_test.go new file mode 100644 index 000000000..1c099b683 --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_addon_test.go @@ -0,0 +1,107 @@ +package heroku + +import ( + "fmt" + "testing" + + "github.com/bgentry/heroku-go" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccHerokuAddon_Basic(t *testing.T) { + var addon heroku.Addon + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuAddonDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckHerokuAddonConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAddonExists("heroku_addon.foobar", &addon), + testAccCheckHerokuAddonAttributes(&addon), + resource.TestCheckResourceAttr( + "heroku_addon.foobar", "config.0.url", "http://google.com"), + resource.TestCheckResourceAttr( + "heroku_addon.foobar", "app", "terraform-test-app"), + resource.TestCheckResourceAttr( + "heroku_addon.foobar", "plan", "deployhooks:http"), + ), + }, + }, + }) +} + +func testAccCheckHerokuAddonDestroy(s *terraform.State) error { + client := testAccProvider.client + + for _, rs := range s.Resources { + if rs.Type != "heroku_addon" { + continue + } + + _, err := client.AddonInfo(rs.Attributes["app"], rs.ID) + + if err == nil { + return fmt.Errorf("Addon still exists") + } + } + + return nil +} + +func testAccCheckHerokuAddonAttributes(addon *heroku.Addon) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if addon.Plan.Name != "deployhooks:http" { + return fmt.Errorf("Bad plan: %s", addon.Plan) + } + + return nil + } +} + +func testAccCheckHerokuAddonExists(n string, addon *heroku.Addon) 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 Addon ID is set") + } + + client := testAccProvider.client + + foundAddon, err := client.AddonInfo(rs.Attributes["app"], rs.ID) + + if err != nil { + return err + } + + if foundAddon.Id != rs.ID { + return fmt.Errorf("Addon not found") + } + + *addon = *foundAddon + + return nil + } +} + +const testAccCheckHerokuAddonConfig_basic = ` +resource "heroku_app" "foobar" { + name = "terraform-test-app" +} + +resource "heroku_addon" "foobar" { + app = "${heroku_app.foobar.name}" + plan = "deployhooks:http" + config { + url = "http://google.com" + } +}` diff --git a/builtin/providers/heroku/resource_heroku_app.go b/builtin/providers/heroku/resource_heroku_app.go index c1f6b728c..5c88cded8 100644 --- a/builtin/providers/heroku/resource_heroku_app.go +++ b/builtin/providers/heroku/resource_heroku_app.go @@ -4,12 +4,45 @@ import ( "fmt" "log" + "github.com/bgentry/heroku-go" + "github.com/hashicorp/terraform/flatmap" "github.com/hashicorp/terraform/helper/config" "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/helper/multierror" "github.com/hashicorp/terraform/terraform" - "github.com/bgentry/heroku-go" ) +// type application is used to store all the details of a heroku app +type application struct { + Id string // Id of the resource + + App *heroku.App // The heroku application + Client *heroku.Client // Client to interact with the heroku API + Vars map[string]string // The vars on the application +} + +// Updates the application to have the latest from remote +func (a *application) Update() error { + var errs []error + var err error + + a.App, err = a.Client.AppInfo(a.Id) + if err != nil { + errs = append(errs, err) + } + + a.Vars, err = retrieve_config_vars(a.Id, a.Client) + if err != nil { + errs = append(errs, err) + } + + if len(errs) > 0 { + return &multierror.Error{Errors: errs} + } + + return nil +} + func resource_heroku_app_create( s *terraform.ResourceState, d *terraform.ResourceDiff, @@ -38,15 +71,29 @@ func resource_heroku_app_create( log.Printf("[DEBUG] App create configuration: %#v", opts) - app, err := client.AppCreate(&opts) + a, err := client.AppCreate(&opts) if err != nil { return s, err } - rs.ID = app.Name - + rs.ID = a.Name log.Printf("[INFO] App ID: %s", rs.ID) + if attr, ok := rs.Attributes["config_vars.#"]; ok && attr == "1" { + vs := flatmap.Expand( + rs.Attributes, "config_vars").([]interface{}) + + err = update_config_vars(rs.ID, vs, client) + if 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) } @@ -54,10 +101,52 @@ func resource_heroku_app_update( s *terraform.ResourceState, d *terraform.ResourceDiff, meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + rs := s.MergeDiff(d) - panic("does not update") + if attr, ok := d.Attributes["name"]; ok { + opts := heroku.AppUpdateOpts{ + Name: &attr.New, + } - return nil, nil + renamedApp, err := client.AppUpdate(rs.ID, &opts) + + if err != nil { + return s, err + } + + // Store the new ID + rs.ID = renamedApp.Name + } + + attr, ok := s.Attributes["config_vars.#"] + + // If the config var block was removed, nuke all config vars + if ok && attr == "1" { + vs := flatmap.Expand( + rs.Attributes, "config_vars").([]interface{}) + + err := update_config_vars(rs.ID, vs, client) + if err != nil { + return rs, err + } + } else if ok && attr == "0" { + log.Println("[INFO] Config vars removed, removing all vars") + + err := update_config_vars(rs.ID, make([]interface{}, 0), client) + + if err != nil { + return rs, err + } + } + + app, err := resource_heroku_app_retrieve(rs.ID, client) + if err != nil { + return rs, err + } + + return resource_heroku_app_update_state(rs, app) } func resource_heroku_app_destroy( @@ -99,9 +188,10 @@ func resource_heroku_app_diff( b := &diff.ResourceBuilder{ Attrs: map[string]diff.AttrType{ - "name": diff.AttrTypeCreate, - "region": diff.AttrTypeUpdate, - "stack": diff.AttrTypeCreate, + "name": diff.AttrTypeUpdate, + "region": diff.AttrTypeUpdate, + "stack": diff.AttrTypeCreate, + "config_vars": diff.AttrTypeUpdate, }, ComputedAttrs: []string{ @@ -111,6 +201,7 @@ func resource_heroku_app_diff( "git_url", "web_url", "id", + "config_vars", }, } @@ -119,26 +210,41 @@ func resource_heroku_app_diff( func resource_heroku_app_update_state( s *terraform.ResourceState, - app *heroku.App) (*terraform.ResourceState, error) { + app *application) (*terraform.ResourceState, error) { - s.Attributes["name"] = app.Name - s.Attributes["stack"] = app.Stack.Name - s.Attributes["region"] = app.Region.Name - s.Attributes["git_url"] = app.GitURL - s.Attributes["web_url"] = app.WebURL - s.Attributes["id"] = app.Id + 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) (*heroku.App, error) { - app, err := client.AppInfo(id) +func resource_heroku_app_retrieve(id string, client *heroku.Client) (*application, error) { + app := application{Id: id, Client: client} + + err := app.Update() if err != nil { return nil, fmt.Errorf("Error retrieving app: %s", err) } - return app, nil + return &app, nil } func resource_heroku_app_validation() *config.Validator { @@ -148,6 +254,38 @@ func resource_heroku_app_validation() *config.Validator { "name", "region", "stack", + "config_vars.*", }, } } + +func retrieve_config_vars(id string, client *heroku.Client) (map[string]string, error) { + vars, err := client.ConfigVarInfo(id) + + if err != nil { + return nil, err + } + + return vars, nil +} + +// Updates the config vars for from an expanded (prior to assertion) +// []map[string]string config +func update_config_vars(id string, vs []interface{}, client *heroku.Client) error { + vars := make(map[string]*string) + + for k, v := range vs[0].(map[string]interface{}) { + val := v.(string) + vars[k] = &val + } + + log.Printf("[INFO] Updating config vars: *%#v", vars) + + _, err := client.ConfigVarUpdate(id, vars) + + if err != nil { + return fmt.Errorf("Error updating config vars: %s", err) + } + + return nil +} diff --git a/builtin/providers/heroku/resource_heroku_app_test.go b/builtin/providers/heroku/resource_heroku_app_test.go index f6f196ce7..ff7c6f110 100644 --- a/builtin/providers/heroku/resource_heroku_app_test.go +++ b/builtin/providers/heroku/resource_heroku_app_test.go @@ -24,6 +24,78 @@ func TestAccHerokuApp_Basic(t *testing.T) { testAccCheckHerokuAppAttributes(&app), resource.TestCheckResourceAttr( "heroku_app.foobar", "name", "terraform-test-app"), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "config_vars.0.FOO", "bar"), + ), + }, + }, + }) +} + +func TestAccHerokuApp_NameChange(t *testing.T) { + var app heroku.App + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuAppDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckHerokuAppConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppAttributes(&app), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "name", "terraform-test-app"), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "config_vars.0.FOO", "bar"), + ), + }, + resource.TestStep{ + Config: testAccCheckHerokuAppConfig_updated, + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppAttributesUpdated(&app), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "name", "terraform-test-renamed"), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "config_vars.0.FOO", "bing"), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "config_vars.0.BAZ", "bar"), + ), + }, + }, + }) +} + +func TestAccHerokuApp_NukeVars(t *testing.T) { + var app heroku.App + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuAppDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckHerokuAppConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppAttributes(&app), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "name", "terraform-test-app"), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "config_vars.0.FOO", "bar"), + ), + }, + resource.TestStep{ + Config: testAccCheckHerokuAppConfig_no_vars, + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppAttributesNoVars(&app), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "name", "terraform-test-app"), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "config_vars.0.FOO", ""), ), }, }, @@ -50,8 +122,76 @@ func testAccCheckHerokuAppDestroy(s *terraform.State) error { func testAccCheckHerokuAppAttributes(app *heroku.App) resource.TestCheckFunc { return func(s *terraform.State) error { + client := testAccProvider.client - // check attrs + if app.Region.Name != "us" { + return fmt.Errorf("Bad region: %s", app.Region.Name) + } + + if app.Stack.Name != "cedar" { + return fmt.Errorf("Bad stack: %s", app.Stack.Name) + } + + if app.Name != "terraform-test-app" { + return fmt.Errorf("Bad name: %s", app.Name) + } + + vars, err := client.ConfigVarInfo(app.Name) + if err != nil { + return err + } + + if vars["FOO"] != "bar" { + return fmt.Errorf("Bad config vars: %v", vars) + } + + return nil + } +} + +func testAccCheckHerokuAppAttributesUpdated(app *heroku.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.client + + if app.Name != "terraform-test-renamed" { + return fmt.Errorf("Bad name: %s", app.Name) + } + + vars, err := client.ConfigVarInfo(app.Name) + if err != nil { + return err + } + + // Make sure we kept the old one + if vars["FOO"] != "bing" { + return fmt.Errorf("Bad config vars: %v", vars) + } + + if vars["BAZ"] != "bar" { + return fmt.Errorf("Bad config vars: %v", vars) + } + + return nil + + } +} + +func testAccCheckHerokuAppAttributesNoVars(app *heroku.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.client + + if app.Name != "terraform-test-app" { + return fmt.Errorf("Bad name: %s", app.Name) + } + + vars, err := client.ConfigVarInfo(app.Name) + if err != nil { + return err + } + + if len(vars) != 0 { + return fmt.Errorf("vars exist: %v", vars) + } return nil } @@ -60,7 +200,7 @@ func testAccCheckHerokuAppAttributes(app *heroku.App) resource.TestCheckFunc { func testAccCheckHerokuAppExists(n string, app *heroku.App) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.Resources[n] - fmt.Printf("resources %#v", s.Resources) + if !ok { return fmt.Errorf("Not found: %s", n) } @@ -81,7 +221,7 @@ func testAccCheckHerokuAppExists(n string, app *heroku.App) resource.TestCheckFu return fmt.Errorf("App not found") } - app = foundApp + *app = *foundApp return nil } @@ -89,5 +229,24 @@ func testAccCheckHerokuAppExists(n string, app *heroku.App) resource.TestCheckFu const testAccCheckHerokuAppConfig_basic = ` resource "heroku_app" "foobar" { - name = "terraform-test-app" + name = "terraform-test-app" + + config_vars { + FOO = "bar" + } +}` + +const testAccCheckHerokuAppConfig_updated = ` +resource "heroku_app" "foobar" { + name = "terraform-test-renamed" + + config_vars { + FOO = "bing" + BAZ = "bar" + } +}` + +const testAccCheckHerokuAppConfig_no_vars = ` +resource "heroku_app" "foobar" { + name = "terraform-test-app" }` diff --git a/builtin/providers/heroku/resource_heroku_domain.go b/builtin/providers/heroku/resource_heroku_domain.go new file mode 100644 index 000000000..6f00fdf55 --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_domain.go @@ -0,0 +1,126 @@ +package heroku + +import ( + "fmt" + "log" + + "github.com/bgentry/heroku-go" + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/terraform" +) + +func resource_heroku_domain_create( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + // Merge the diff into the state so that we have all the attributes + // properly. + rs := s.MergeDiff(d) + + app := rs.Attributes["app"] + hostname := rs.Attributes["hostname"] + + log.Printf("[DEBUG] Domain create configuration: %#v, %#v", app, hostname) + + do, err := client.DomainCreate(app, hostname) + + if err != nil { + return s, err + } + + rs.ID = do.Id + rs.Attributes["hostname"] = do.Hostname + rs.Attributes["cname"] = fmt.Sprintf("%s.herokuapp.com", app) + + log.Printf("[INFO] Domain ID: %s", rs.ID) + + return rs, nil +} + +func resource_heroku_domain_update( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + + panic("Cannot update domain") + + return nil, nil +} + +func resource_heroku_domain_destroy( + s *terraform.ResourceState, + meta interface{}) error { + p := meta.(*ResourceProvider) + client := p.client + + log.Printf("[INFO] Deleting Domain: %s", s.ID) + + // Destroy the app + err := client.DomainDelete(s.Attributes["app"], s.ID) + + if err != nil { + return fmt.Errorf("Error deleting domain: %s", err) + } + + return nil +} + +func resource_heroku_domain_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + domain, err := resource_heroku_domain_retrieve(s.Attributes["app"], s.ID, client) + if err != nil { + return nil, err + } + + s.Attributes["hostname"] = domain.Hostname + s.Attributes["cname"] = fmt.Sprintf("%s.herokuapp.com", s.Attributes["app"]) + + return s, nil +} + +func resource_heroku_domain_diff( + s *terraform.ResourceState, + c *terraform.ResourceConfig, + meta interface{}) (*terraform.ResourceDiff, error) { + + b := &diff.ResourceBuilder{ + Attrs: map[string]diff.AttrType{ + "hostname": diff.AttrTypeCreate, + "app": diff.AttrTypeCreate, + }, + + ComputedAttrs: []string{ + "cname", + }, + } + + return b.Diff(s, c) +} + +func resource_heroku_domain_retrieve(app string, id string, client *heroku.Client) (*heroku.Domain, error) { + domain, err := client.DomainInfo(app, id) + + if err != nil { + return nil, fmt.Errorf("Error retrieving domain: %s", err) + } + + return domain, nil +} + +func resource_heroku_domain_validation() *config.Validator { + return &config.Validator{ + Required: []string{ + "hostname", + "app", + }, + Optional: []string{}, + } +} diff --git a/builtin/providers/heroku/resource_heroku_domain_test.go b/builtin/providers/heroku/resource_heroku_domain_test.go new file mode 100644 index 000000000..315881690 --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_domain_test.go @@ -0,0 +1,104 @@ +package heroku + +import ( + "fmt" + "testing" + + "github.com/bgentry/heroku-go" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccHerokuDomain_Basic(t *testing.T) { + var domain heroku.Domain + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuDomainDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckHerokuDomainConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuDomainExists("heroku_domain.foobar", &domain), + testAccCheckHerokuDomainAttributes(&domain), + resource.TestCheckResourceAttr( + "heroku_domain.foobar", "hostname", "terraform.example.com"), + resource.TestCheckResourceAttr( + "heroku_domain.foobar", "app", "terraform-test-app"), + resource.TestCheckResourceAttr( + "heroku_domain.foobar", "cname", "terraform-test-app.herokuapp.com"), + ), + }, + }, + }) +} + +func testAccCheckHerokuDomainDestroy(s *terraform.State) error { + client := testAccProvider.client + + for _, rs := range s.Resources { + if rs.Type != "heroku_domain" { + continue + } + + _, err := client.DomainInfo(rs.Attributes["app"], rs.ID) + + if err == nil { + return fmt.Errorf("Domain still exists") + } + } + + return nil +} + +func testAccCheckHerokuDomainAttributes(Domain *heroku.Domain) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if Domain.Hostname != "terraform.example.com" { + return fmt.Errorf("Bad hostname: %s", Domain.Hostname) + } + + return nil + } +} + +func testAccCheckHerokuDomainExists(n string, Domain *heroku.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 Domain ID is set") + } + + client := testAccProvider.client + + foundDomain, err := client.DomainInfo(rs.Attributes["app"], rs.ID) + + if err != nil { + return err + } + + if foundDomain.Id != rs.ID { + return fmt.Errorf("Domain not found") + } + + *Domain = *foundDomain + + return nil + } +} + +const testAccCheckHerokuDomainConfig_basic = ` +resource "heroku_app" "foobar" { + name = "terraform-test-app" +} + +resource "heroku_domain" "foobar" { + app = "${heroku_app.foobar.name}" + hostname = "terraform.example.com" +}` diff --git a/builtin/providers/heroku/resources.go b/builtin/providers/heroku/resources.go index 00140d3f7..a208714c6 100644 --- a/builtin/providers/heroku/resources.go +++ b/builtin/providers/heroku/resources.go @@ -11,6 +11,15 @@ 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, @@ -19,6 +28,14 @@ func init() { 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, + }, }, } } diff --git a/command/flag_var.go b/command/flag_var.go index 41450366f..54bd783a4 100644 --- a/command/flag_var.go +++ b/command/flag_var.go @@ -55,7 +55,7 @@ func (v *FlagVarFile) Set(raw string) error { return nil } -const libuclParseFlags = libucl.ParserKeyLowercase +const libuclParseFlags = libucl.ParserNoTime func loadVarFile(path string) (map[string]string, error) { var obj *libucl.Object diff --git a/config.go b/config.go index 049bd38de..29a30e66c 100644 --- a/config.go +++ b/config.go @@ -29,13 +29,14 @@ var ContextOpts terraform.ContextOpts // Put the parse flags we use for libucl in a constant so we can get // equally behaving parsing everywhere. -const libuclParseFlags = libucl.ParserKeyLowercase +const libuclParseFlags = libucl.ParserNoTime func init() { BuiltinConfig.Providers = map[string]string{ "aws": "terraform-provider-aws", "digitalocean": "terraform-provider-digitalocean", "heroku": "terraform-provider-heroku", + "dnsimple": "terraform-provider-dnsimple", } BuiltinConfig.Provisioners = map[string]string{ "local-exec": "terraform-provisioner-local-exec", diff --git a/config/loader_libucl.go b/config/loader_libucl.go index 2599a6d16..2471a9b77 100644 --- a/config/loader_libucl.go +++ b/config/loader_libucl.go @@ -9,7 +9,7 @@ import ( // Put the parse flags we use for libucl in a constant so we can get // equally behaving parsing everywhere. -const libuclParseFlags = libucl.ParserKeyLowercase +const libuclParseFlags = libucl.ParserNoTime // libuclConfigurable is an implementation of configurable that knows // how to turn libucl configuration into a *Config object. diff --git a/config/loader_test.go b/config/loader_test.go index 168f4e931..c154075e6 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -485,6 +485,7 @@ do const basicResourcesStr = ` aws_instance[db] (x1) + VPC security_groups dependsOn aws_instance.web diff --git a/config/test-fixtures/basic.tf b/config/test-fixtures/basic.tf index a8938c018..fe3267c8a 100644 --- a/config/test-fixtures/basic.tf +++ b/config/test-fixtures/basic.tf @@ -31,6 +31,7 @@ resource aws_instance "web" { resource "aws_instance" "db" { security_groups = "${aws_security_group.firewall.*.id}" + VPC = "foo" depends_on = ["aws_instance.web"] } diff --git a/config/test-fixtures/basic.tf.json b/config/test-fixtures/basic.tf.json index 7c43b5b30..6c6ff00bd 100644 --- a/config/test-fixtures/basic.tf.json +++ b/config/test-fixtures/basic.tf.json @@ -21,6 +21,7 @@ "aws_instance": { "db": { "security_groups": ["${aws_security_group.firewall.*.id}"], + "VPC": "foo", "depends_on": ["aws_instance.web"] }, diff --git a/website/source/community.html.erb b/website/source/community.html.erb index ce58ebf27..43775e978 100644 --- a/website/source/community.html.erb +++ b/website/source/community.html.erb @@ -1,93 +1,80 @@ --- +layout: "inner" page_title: "Community" --- -
-
-
-

Community

+

Community

+

+Terraform is a new project with a growing community. Despite this, +there are active, dedicated users willing to help you through various +mediums. +

+

+IRC: #terraform on Freenode +

+

+Mailing list: +Terraform Google Group +

+

+Bug Tracker: +Issue tracker + on GitHub. Please only use this for reporting bugs. Do not ask +for general help here. Use IRC or the mailing list for that. + +

People

+

+The following people are some of the faces behind Terraform. They each +contribute to Terraform in some core way. Over time, faces may appear and +disappear from this list as contributors come and go. +

+
+
+ +
+

Mitchell Hashimoto (@mitchellh)

- Terraform is a new project with a growing community. Despite this, - there are active, dedicated users willing to help you through various - mediums. + Mitchell Hashimoto is the creator of Terraform and works on all + layers of Terraform from the core to providers. In addition to Terraform, + Mitchell is the creator of + Vagrant, + Packer, and + Consul.

-

- IRC: #terraform on Freenode -

-

- Mailing list: - Terraform Google Group -

-

- Bug Tracker: - Issue tracker - on GitHub. Please only use this for reporting bugs. Do not ask - for general help here. Use IRC or the mailing list for that. - -

People

-

- The following people are some of the faces behind Terraform. They each - contribute to Terraform in some core way. Over time, faces may appear and - disappear from this list as contributors come and go. -

-
-
- -
-

Armon Dadgar (@armon)

-

- Armon Dadgar is the creator of Terraform. He researched and developed - most of the internals of how Terraform works, including the - gossip layer, leader election, etc. Armon is also the creator of - Serf, - Statsite, and - Bloomd. -

-
- -
- -
-

Mitchell Hashimoto (@mitchellh)

-

- Mitchell Hashimoto is a co-creator of Terraform. He primarily took - a management role in the creation of Terraform, guiding product - and user experience decisions on top of Armon's technical decisions. - Mitchell Hashimoto is also the creator of - Vagrant, - Packer, and - Serf. -

-
-
- -
- -
-

Jack Pearkes (@pearkes)

-

- Jack Pearkes created and maintains the Terraform web UI. - He is also a core committer to - Packer and maintains - many successful - open source projects - while also being an employee of - HashiCorp. -

-
-
- -
- -
-

William Tisäter (@tiwilliam)

-

William Tisäter is a Terraform core committer. He is also maintainer of pygeoip and build things daily at Tictail.

-
-
- -
-
+ +
+ +
+

Armon Dadgar (@armon)

+

+ Armon Dadgar is a creator of Terraform. He created valuable sections + of the core and helps maintain providers as well. Armon is also the + creator of + Consul, + Serf, + Statsite, and + Bloomd. +

+
+ +
+ +
+

Jack Pearkes (@pearkes)

+

+ Jack Pearkes is a creator of Terraform. He created and maintains + most of the providers and documentation. + He is also a core committer to + Packer and + Consul + while also being an employee of + HashiCorp. +

+
+
+ +
diff --git a/website/source/docs/providers/aws/index.html.markdown b/website/source/docs/providers/aws/index.html.markdown new file mode 100644 index 000000000..8105d8b83 --- /dev/null +++ b/website/source/docs/providers/aws/index.html.markdown @@ -0,0 +1,43 @@ +--- +layout: "aws" +page_title: "Provider: AWS" +sidebar_current: "docs-aws-index" +--- + +# AWS Provider + +The Amazon Web Services (AWS) provider is used to interact with the +many resources supported by AWS. 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 AWS Provider +provider "aws" { + access_key = "${var.aws_access_key}" + secret_key = "${var.aws_secret_key}" + region = "us-east-1" +} + +# Create a web server +resource "aws_instance" "web" { + ... +} +``` + +## Argument Reference + +The following arguments are supported: + +* `access_key` - (Required) This is the AWS access key. It must be provided, but + it can also be sourced from the `AWS_ACCESS_KEY` environment variable. + +* `secret_key` - (Required) This is the AWS secret key. It must be provided, but + it can also be sourced from the `AWS_SECRET_KEY` environment variable. + +* `region` - (Required) This is the AWS region. It must be provided, but + it can also be sourced from the `AWS_REGION` environment variables. + diff --git a/website/source/docs/providers/aws/r/eip.html.markdown b/website/source/docs/providers/aws/r/eip.html.markdown new file mode 100644 index 000000000..c70f7b813 --- /dev/null +++ b/website/source/docs/providers/aws/r/eip.html.markdown @@ -0,0 +1,33 @@ +--- +layout: "aws" +page_title: "AWS: aws_eip" +sidebar_current: "docs-aws-resource-eip" +--- + +# aws\_eip + +Provides an Elastic IP resource. + +## Example Usage + +``` +resource "aws_eip" "lb" { + instance = "${aws_instance.web.instance_id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `vpc` - (Optional) VPC ID +* `instance` - (Optional) EC2 instance ID. + +## Attributes Reference + +The following attributes are exported: + +* `private_ip` - Contrains the private IP address (if in VPC). +* `public_ip` - Contains the public IP address. +* `instance` - Contains the ID of the instance attached ot. + diff --git a/website/source/docs/providers/aws/r/instance.html.markdown b/website/source/docs/providers/aws/r/instance.html.markdown new file mode 100644 index 000000000..e1356ef20 --- /dev/null +++ b/website/source/docs/providers/aws/r/instance.html.markdown @@ -0,0 +1,49 @@ +--- +layout: "aws" +page_title: "AWS: aws_instance" +sidebar_current: "docs-aws-resource-instance" +--- + +# aws\_instance + +Provides an EC2 instance resource. This allows instances to be created, updated, +and deleted. Instances also support [provisioning](/docs/provisioners/index.html). + +## Example Usage + +``` +# Create a new instance of the ami-1234 on an m1.small node +resource "aws_instance" "web" { + ami = "ami-1234" + instance_type = "m1.small" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `ami` - (Required) The AMI to use for the instance. +* `availability_zone` - (Optional) The AZ to start the instance in. +* `instance_type` - (Required) The type of instance to start +* `key_name` - (Optional) The key name to use for the instance. +* `security_groups` - (Optional) A list of security group IDs to associate with. +* `subnet_id` - (Optional) The VPC Subnet ID to launch in. +* `source_dest_check` - (Optional) Controls if traffic is routed to the instance when + the destination address does not match the instance. Used for NAT or VPNs. Defaults false. +* `user_data` - (Optional) The user data to provide when launching the instance. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The instance ID. +* `availability_zone` - The availability zone of the instance. +* `key_name` - The key name of the instance +* `private_dns` - The Private DNS name of the instance +* `private_ip` - The private IP address. +* `public_dns` - The public DNS name of the instance +* `public_ip` - The public IP address. +* `security_groups` - The associated security groups. +* `subnet_id` - The VPC subnet ID. + diff --git a/website/source/docs/providers/aws/r/internet_gateway.html.markdown b/website/source/docs/providers/aws/r/internet_gateway.html.markdown new file mode 100644 index 000000000..e83ec6871 --- /dev/null +++ b/website/source/docs/providers/aws/r/internet_gateway.html.markdown @@ -0,0 +1,30 @@ +--- +layout: "aws" +page_title: "AWS: aws_internet_gateway" +sidebar_current: "docs-aws-resource-internet-gateway" +--- + +# aws\_internet\_gateway + +Provides a resource to create a VPC Internet Gateway. + +## Example Usage + +``` +resource "aws_internet_gateway" "gw" { + vpc_id = "${aws_vpc.main.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `vpc_id` - (Required) The VPC ID to create in. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the Internet Gateway. + diff --git a/website/source/docs/providers/aws/r/launch_config.html.markdown b/website/source/docs/providers/aws/r/launch_config.html.markdown new file mode 100644 index 000000000..4c697d9fa --- /dev/null +++ b/website/source/docs/providers/aws/r/launch_config.html.markdown @@ -0,0 +1,36 @@ +--- +layout: "aws" +page_title: "AWS: aws_launch_configuration" +sidebar_current: "docs-aws-resource-launch-config" +--- + +# aws\_launch\_configuration + +Provides a resource to create a new launch configuration, used for autoscaling groups. + +## Example Usage + +``` +resource "aws_launch_configuration" "as_conf" { + name = "web_config" + image_id = "ami-1234" + instance_type = "m1.small" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the launch configuration. +* `image_id` - (Required) The EC2 image ID to launch. +* `instance_type` - (Required) The size of instance to launch. +* `key_name` - (Optional) The key name that should be used for the instance. +* `security_groups` - (Optional) A list of associated security group IDS. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the launch configuration. + diff --git a/website/source/docs/providers/aws/r/route53_record.html.markdown b/website/source/docs/providers/aws/r/route53_record.html.markdown new file mode 100644 index 000000000..4a519cf82 --- /dev/null +++ b/website/source/docs/providers/aws/r/route53_record.html.markdown @@ -0,0 +1,36 @@ +--- +layout: "aws" +page_title: "AWS: aws_route53_record" +sidebar_current: "docs-aws-resource-route53-record" +--- + +# aws\_route53\_record + +Provides a Route53 record resource. + +## Example Usage + +``` +resource "aws_route53_record" "www" { + zone_id = "${aws_route53_zone.primary.zone_id}" + name = "www.example.com" + type = "A" + ttl = "300" + records = ["${aws_eip.lb.public_ip}"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `zone_id` - (Required) The ID of the hosted zone to contain this record. +* `name` - (Required) The name of the record. +* `type` - (Required) The record type. +* `ttl` - (Required) The TTL of the record. +* `records` - (Required) A string list of records. + +## Attributes Reference + +No attributes are exported. + diff --git a/website/source/docs/providers/aws/r/route53_zone.html.markdown b/website/source/docs/providers/aws/r/route53_zone.html.markdown new file mode 100644 index 000000000..6be339aaf --- /dev/null +++ b/website/source/docs/providers/aws/r/route53_zone.html.markdown @@ -0,0 +1,30 @@ +--- +layout: "aws" +page_title: "AWS: aws_route53_zone" +sidebar_current: "docs-aws-resource-route53-zone" +--- + +# aws\_route53\_zone + +Provides a Route53 Hosted Zone resource. + +## Example Usage + +``` +resource "aws_route53_zone" "primary" { + name = "example.com" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) This is the name of the hosted zone. + +## Attributes Reference + +The following attributes are exported: + +* `zone_id` - The Hosted Zone ID. This can be referenced by zone records. + diff --git a/website/source/docs/providers/aws/r/route_table.html.markdown b/website/source/docs/providers/aws/r/route_table.html.markdown new file mode 100644 index 000000000..fdde1d252 --- /dev/null +++ b/website/source/docs/providers/aws/r/route_table.html.markdown @@ -0,0 +1,40 @@ +--- +layout: "aws" +page_title: "AWS: aws_route_table" +sidebar_current: "docs-aws-resource-route-table|" +--- + +# aws\_route\_table + +Provides a resource to create a VPC routing table. + +## Example Usage + +``` +resource "aws_route_table" "r" { + vpc_id = "${aws_vpc.default.id}" + route { + cidr_block = "10.0.1.0/24" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `vpc_id` - (Required) The ID of the routing table. +* `route` - (Optional) A list of route objects. Their keys are documented below. + +Each route supports the following: + +* `cidr_block` - (Required) The CIDR block of the route. +* `gateway_id` - (Optional) The Internet Gateway ID. +* `instance_id` - (Optional) The EC2 instance ID. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the routing table + diff --git a/website/source/docs/providers/aws/r/route_table_assoc.html.markdown b/website/source/docs/providers/aws/r/route_table_assoc.html.markdown new file mode 100644 index 000000000..2c924b7d2 --- /dev/null +++ b/website/source/docs/providers/aws/r/route_table_assoc.html.markdown @@ -0,0 +1,32 @@ +--- +layout: "aws" +page_title: "AWS: aws_route_table_association" +sidebar_current: "docs-aws-resource-route-table-assoc" +--- + +# aws\_route\_table\_association + +Provides a resource to create an association between a subnet and routing table. + +## Example Usage + +``` +resource "aws_route_table_association" "a" { + subnet_id = "${aws_subnet.foo.id}" + route_table_id = "${aws_route_table.bar.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `subnet_id` - (Required) The subnet ID to create an association. +* `route_table_id` - (Required) The ID of the routing table to associate with. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the association + diff --git a/website/source/docs/providers/aws/r/s3_bucket.html.markdown b/website/source/docs/providers/aws/r/s3_bucket.html.markdown new file mode 100644 index 000000000..dcce52738 --- /dev/null +++ b/website/source/docs/providers/aws/r/s3_bucket.html.markdown @@ -0,0 +1,32 @@ +--- +layout: "aws" +page_title: "AWS: aws_s3_bucket" +sidebar_current: "docs-aws-resource-s3-bucket" +--- + +# aws\_s3\_bucket + +Provides a S3 bucket resource. + +## Example Usage + +``` +resource "aws_s3_bucket" "b" { + bucket = "my_tf_test_bucket" + acl = "private" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `bucket` - (Required) The name of the bucket. +* `acl` - (Optional) The canned ACL to apply. Defaults to "private". + +## Attributes Reference + +The following attributes are exported: + +* `id` - The name of the bucket + diff --git a/website/source/docs/providers/aws/r/security_group.html.markdown b/website/source/docs/providers/aws/r/security_group.html.markdown new file mode 100644 index 000000000..ea226af4c --- /dev/null +++ b/website/source/docs/providers/aws/r/security_group.html.markdown @@ -0,0 +1,54 @@ +--- +layout: "aws" +page_title: "AWS: aws_security_group" +sidebar_current: "docs-aws-resource-security-group" +--- + +# aws\_security\_group + +Provides an security group resource. + +## Example Usage + +``` +resource "aws_security_group" "allow_all" { + name = "allow_all" + ingress { + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the security group +* `ingress` - (Required) Can be specified multiple times for each + ingress rule. Each ingress block supports fields documented below. +* `description` - (Optional) The security group description. +* `vpc_id` - (Optional) The VPC ID. +* `owner_id` - (Optional) The AWS Owner ID. + +The `ingress` block supports: + +* `cidr_blocks` - (Optional) List of CIDR blocks. Cannot be used with `security_groups`. +* `from_port` - (Required) The start port. +* `protocol` - (Required) The protocol. +* `security_groups` - (Optional) List of security group IDs. Cannot be used with `cidr_blocks`. +* `to_port` - (Required) The end range port. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the security group +* `vpc_id` - The VPC ID. +* `owner_id` - The owner ID. +* `name` - The name of the security group +* `description` - The description of the security group +* `ingress` - The ingress rules. See above for more. + diff --git a/website/source/docs/providers/aws/r/subnet.html.markdown b/website/source/docs/providers/aws/r/subnet.html.markdown new file mode 100644 index 000000000..358ed720b --- /dev/null +++ b/website/source/docs/providers/aws/r/subnet.html.markdown @@ -0,0 +1,36 @@ +--- +layout: "aws" +page_title: "AWS: aws_subnet" +sidebar_current: "docs-aws-resource-subnet" +--- + +# aws\_subnet + +Provides an VPC subnet resource. + +## Example Usage + +``` +resource "aws_subnet" "main" { + vpc_id = "${aws_vpc.main.id}" + cidr_block = "10.0.1.0/16" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `availability_zone`- (Optional) The AZ for the subnet. +* `cidr_block` - (Required) The CIDR block for the subnet. +* `vpc_id` - (Required) The VPC ID. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the subnet +* `availability_zone`- The AZ for the subnet. +* `cidr_block` - The CIDR block for the subnet. +* `vpc_id` - The VPC ID. + diff --git a/website/source/docs/providers/aws/r/vpc.html.markdown b/website/source/docs/providers/aws/r/vpc.html.markdown new file mode 100644 index 000000000..9f16b82d8 --- /dev/null +++ b/website/source/docs/providers/aws/r/vpc.html.markdown @@ -0,0 +1,31 @@ +--- +layout: "aws" +page_title: "AWS: aws_vpc" +sidebar_current: "docs-aws-resource-vpc" +--- + +# aws\_vpc + +Provides an VPC resource. + +## Example Usage + +``` +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `cidr_block` - (Required) The CIDR block for the VPC. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the VPC +* `cidr_block` - The CIDR block of the VPC + diff --git a/website/source/docs/providers/index.html.markdown b/website/source/docs/providers/index.html.markdown new file mode 100644 index 000000000..fb739259d --- /dev/null +++ b/website/source/docs/providers/index.html.markdown @@ -0,0 +1,18 @@ +--- +layout: "docs" +page_title: "Providers" +sidebar_current: "docs-providers" +--- + +# Providers + +Terraform is used to create, manage, and manipulate infrastructure resources. +Examples of resources include physical machines, VMs, network switches, containers, +etc. Almost any infrastructure noun can be represented as a resource in Terraform. + +Terraform is agnostic to the underlying platforms by supporting providers. A provider +is responsible for understanding API interactions and exposing resources. Providers +generally are an IaaS (e.g. AWS, DigitalOcean, GCE), PaaS (e.g. Heroku, CloudFoundry), +or SaaS services (e.g. DNSimple, CloudFlare). + +Use the navigation to the left to read about the available providers. diff --git a/website/source/docs/provisioners/connection.html.markdown b/website/source/docs/provisioners/connection.html.markdown new file mode 100644 index 000000000..4206ca990 --- /dev/null +++ b/website/source/docs/provisioners/connection.html.markdown @@ -0,0 +1,53 @@ +--- +layout: "docs" +page_title: "Provisioner Connections" +sidebar_current: "docs-provisioners-connection" +--- + +# Provisioner Connections + +Many provisioners require access to the remote resource. For example, +a provisioner may need to use ssh to connect to the resource. + +Terraform uses a number of defaults when connecting to a resource, but these +can be overriden using `connection` block in either a `resource` or `provisioner`. +Any `connection` information provided in a `resource` will apply to all the +provisioners, but it can be scoped to a single provisioner as well. One use case +is to have an initial provisioner connect as root to setup user accounts, and have +subsequent provisioners connect as a user with more limited permissions. + +## Example usage + +``` +# Copies the file as the root user using a password +provisioner "file" { + source = "conf/myapp.conf" + destination = "/etc/myapp.conf" + connection { + user = "root" + password = "${var.root_password}" + } +} +``` + +## Argument Reference + +The following arugments are supported: + +* `type` - The connection type that should be used. This defaults to "ssh". The type + of connection supported depends on the provisioner. + +* `user` - The user that we should use for the connection. This defaults to "root". + +* `password` - The password we should use for the connection. + +* `key_file` - The SSH key to use for the connection. This takes preference over the + password if provided. + +* `host` - The address of the resource to connect to. This is provided by the provider. + +* `port` - The port to connect to. This defaults to 22. + +* `timeout` - The timeout to wait for the conneciton to become available. This defaults + to 5 minutes. Should be provided as a string like "30s" or "5m". + diff --git a/website/source/docs/provisioners/file.html.markdown b/website/source/docs/provisioners/file.html.markdown new file mode 100644 index 000000000..d3f8457da --- /dev/null +++ b/website/source/docs/provisioners/file.html.markdown @@ -0,0 +1,64 @@ +--- +layout: "docs" +page_title: "Provisioner: file" +sidebar_current: "docs-provisioners-file" +--- + +# File Provisioner + +The `file` provisioner is used to copy files or directories from the machine +executing Terraform to the newly created resource. The `file` provisioner only +supports `ssh` type [connections](/docs/provisioners/connection.html). + +## Example usage + +``` +resource "aws_instance" "web" { + ... + + # Copies the myapp.conf file to /etc/myapp.conf + provisioner "file" { + source = "conf/myapp.conf" + destination = "/etc/myapp.conf" + } + + # Copies the configs.d folder to /etc/configs.d + provisioner "file" { + source = "conf/configs.d" + destination = "/etc" + } +} +``` + +## Argument Reference + +The following arugments are supported: + +* `source` - (Required) This is the source file or folder. It can be specified as relative + to the current working directory or as an absolute path. + +* `destination` - (Required) This is the destination path. It must be specified as an + absolute path. + +## Directory Uploads + +The file provisioner is also able to upload a complete directory to the remote machine. +When uploading a directory, there are a few important things you should know. + +First, the destination directory must already exist. If you need to create it, +use a remote-exec provisioner just prior to the file provisioner in order to create the directory. + +Next, the existence of a trailing slash on the source path will determine whether the +directory name will be embedded within the destination, or whether the destination will +be created. An example explains this best: + +If the source is `/foo` (no trailing slash), and the destination is `/tmp`, then the contents +of `/foo` on the local machine will be uploaded to `/tmp/foo` on the remote machine. The +`foo` directory on the remote machine will be created by Terraform. + +If the source, however, is `/foo/` (a trailing slash is present), and the destination is +`/tmp`, then the contents of `/foo` will be uploaded directly into `/tmp` directly. + +This behavior was adopted from the standard behavior of rsync. Note that under the covers, +rsync may or may not be used. + diff --git a/website/source/docs/provisioners/index.html.markdown b/website/source/docs/provisioners/index.html.markdown new file mode 100644 index 000000000..8fc766a3c --- /dev/null +++ b/website/source/docs/provisioners/index.html.markdown @@ -0,0 +1,15 @@ +--- +layout: "docs" +page_title: "Provisioners" +sidebar_current: "docs-provisioners" +--- + +# Provisioners + +When a resource is initially created, provisioners can be executed to +initialize that resource. This can be used to add resources to an inventory +management system, run a configuration management tool, bootstrap the +resource into a cluster, etc. + +Use the navigation to the left to read about the available provisioners. + diff --git a/website/source/docs/provisioners/local-exec.html.markdown b/website/source/docs/provisioners/local-exec.html.markdown new file mode 100644 index 000000000..5ac15c498 --- /dev/null +++ b/website/source/docs/provisioners/local-exec.html.markdown @@ -0,0 +1,34 @@ +--- +layout: "docs" +page_title: "Provisioner: local-exec" +sidebar_current: "docs-provisioners-local" +--- + +# local-exec Provisioner + +The `local-exec` provisioner invokes a local executable after a resource +is created. This invokes a process on the machine running Terraform, not on +the resource. See the `remote-exec` [provisioner](/docs/provisioners/remote-exec.html) +to run commands on the resource. + +## Example usage + +``` +# Join the newly created machine to our Consul cluster +resource "aws_instance" "web" { + ... + provisioner "local-exec" { + command = "consul join ${aws_instance.web.private_ip}" + } +} +``` + +## Argument Reference + +The following arugments are supported: + +* `command` - (Required) This is the command to execute. It can be provided + as a relative path to the current working directory or as an absolute path. + It is evaluated in a shell, and can use environment variables or Terraform + variables. + diff --git a/website/source/docs/provisioners/remote-exec.html.markdown b/website/source/docs/provisioners/remote-exec.html.markdown new file mode 100644 index 000000000..0b6123115 --- /dev/null +++ b/website/source/docs/provisioners/remote-exec.html.markdown @@ -0,0 +1,45 @@ +--- +layout: "docs" +page_title: "Provisioner: remote-exec" +sidebar_current: "docs-provisioners-remote" +--- + +# remote-exec Provisioner + +The `remote-exec` provisioner invokes a script on a remote resource after it +is created. This can be used to run a configuration management tool, bootstrap +into a cluster, etc. To invoke a local process, see the `local-exec` +[provisioner](/docs/provisioners/local-exec.html) instead. The `remote-exec` +provisioner only supports `ssh` type [connections](/docs/provisioners/connection.html). + + +## Example usage + +``` +# Run puppet and join our Consul cluster +resource "aws_instance" "web" { + ... + provisioner "remote-exec" { + inline = [ + "puppet apply", + "consul join ${aws_instance.web.private_ip", + ] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `inline` - This is a list of command strings. They are executed in the order + they are provided. This cannot be provided with `script` or `scripts`. + +* `script` - This is a path (relative or absolute) to a local script that will + be copied to the remote resource and then executed. This cannot be provided + with `inline` or `scripts`. + +* `scripts` - This is a list of paths (relative or absolute) to local scripts + that will be copied to the remote resource and then executed. They are executed + in the order they are provided. This cannot be provided with `inline` or `script`. + diff --git a/website/source/intro/getting-started/agent.html.markdown b/website/source/intro/getting-started/agent.html.markdown deleted file mode 100644 index 2400432c8..000000000 --- a/website/source/intro/getting-started/agent.html.markdown +++ /dev/null @@ -1,125 +0,0 @@ ---- -layout: "intro" -page_title: "Run the Agent" -sidebar_current: "gettingstarted-agent" ---- - -# Run the Terraform Agent - -After Terraform is installed, the agent must be run. The agent can either run -in a server or client mode. Each datacenter must have at least one server, -although 3 or 5 is recommended. A single server deployment is _**highly**_ discouraged -as data loss is inevitable in a failure scenario. [This guide](/docs/guides/bootstrapping.html) -covers bootstrapping a new datacenter. All other agents run in client mode, which -is a very lightweight process that registers services, runs health checks, -and forwards queries to servers. The agent must be run for every node that -will be part of the cluster. - -## Starting the Agent - -For simplicity, we'll run a single Terraform agent in server mode right now: - -``` -$ terraform agent -server -bootstrap -data-dir /tmp/consul -==> WARNING: Bootstrap mode enabled! Do not enable unless necessary -==> WARNING: It is highly recommended to set GOMAXPROCS higher than 1 -==> Starting Terraform agent... -==> Starting Terraform agent RPC... -==> Terraform agent running! - Node name: 'Armons-MacBook-Air' - Datacenter: 'dc1' - Server: true (bootstrap: true) - Client Addr: 127.0.0.1 (HTTP: 8500, DNS: 8600, RPC: 8400) - Cluster Addr: 10.1.10.38 (LAN: 8301, WAN: 8302) - -==> Log data will now stream in as it occurs: - -[INFO] serf: EventMemberJoin: Armons-MacBook-Air.local 10.1.10.38 -[INFO] raft: Node at 10.1.10.38:8300 [Follower] entering Follower state -[INFO] terraform: adding server for datacenter: dc1, addr: 10.1.10.38:8300 -[ERR] agent: failed to sync remote state: rpc error: No cluster leader -[WARN] raft: Heartbeat timeout reached, starting election -[INFO] raft: Node at 10.1.10.38:8300 [Candidate] entering Candidate state -[INFO] raft: Election won. Tally: 1 -[INFO] raft: Node at 10.1.10.38:8300 [Leader] entering Leader state -[INFO] terraform: cluster leadership acquired -[INFO] terraform: New leader elected: Armons-MacBook-Air -[INFO] terraform: member 'Armons-MacBook-Air' joined, marking health alive -``` - -As you can see, the Terraform agent has started and has output some log -data. From the log data, you can see that our agent is running in server mode, -and has claimed leadership of the cluster. Additionally, the local member has -been marked as a healthy member of the cluster. - -
-Note for OS X Users: Terraform uses your hostname as the -default node name. If your hostname contains periods, DNS queries to -that node will not work with Terraform. To avoid this, explicitly set -the name of your node with the -node flag. -
- -## Cluster Members - -If you run `terraform members` in another terminal, you can see the members of -the Terraform cluster. You should only see one member (yourself). We'll cover -joining clusters in the next section. - -``` -$ terraform members -Armons-MacBook-Air 10.1.10.38:8301 alive role=terraform,dc=dc1,vsn=1,vsn_min=1,vsn_max=1,port=8300,bootstrap=1 -``` - -The output shows our own node, the address it is running on, its -health state, and some metadata associated with the node. Some important -metadata keys to recognize are the `role` and `dc` keys. These tell you -the service name and the datacenter that member is within. These can be -used to lookup nodes and services using the DNS interface, which is covered -shortly. - -The output from the `members` command is generated based on the -[gossip protocol](/docs/internals/gossip.html) and is eventually consistent. -For a strongly consistent view of the world, use the -[HTTP API](/docs/agent/http.html), which forwards the request to the -Terraform servers: - -``` -$ curl localhost:8500/v1/catalog/nodes -[{"Node":"Armons-MacBook-Air","Address":"10.1.10.38"}] -``` - -In addition to the HTTP API, the -[DNS interface](/docs/agent/dns.html) can be used to query the node. Note -that you have to make sure to point your DNS lookups to the Terraform agent's -DNS server, which runs on port 8600 by default. The format of the DNS -entries (such as "Armons-MacBook-Air.node.terraform") will be covered later. - -``` -$ dig @127.0.0.1 -p 8600 Armons-MacBook-Air.node.terraform -... - -;; QUESTION SECTION: -;Armons-MacBook-Air.node.terraform. IN A - -;; ANSWER SECTION: -Armons-MacBook-Air.node.terraform. 0 IN A 10.1.10.38 -``` - -## Stopping the Agent - -You can use `Ctrl-C` (the interrupt signal) to gracefully halt the agent. -After interrupting the agent, you should see it leave the cluster gracefully -and shut down. - -By gracefully leaving, Terraform notifies other cluster members that the -node _left_. If you had forcibly killed the agent process, other members -of the cluster would have detected that the node _failed_. When a member leaves, -its services and checks are removed from the catalog. When a member fails, -its health is simply marked as critical, but is not removed from the catalog. -Terraform will automatically try to reconnect to _failed_ nodes, which allows it -to recover from certain network conditions, while _left_ nodes are no longer contacted. - -Additionally, if an agent is operating as a server, a graceful leave is important -to avoid causing a potential availability outage affecting the [consensus protocol](/docs/internals/consensus.html). -See the [guides section](/docs/guides/index.html) to safely add and remove servers. - diff --git a/website/source/intro/getting-started/build.html.md b/website/source/intro/getting-started/build.html.md new file mode 100644 index 000000000..e0a19274b --- /dev/null +++ b/website/source/intro/getting-started/build.html.md @@ -0,0 +1,203 @@ +--- +layout: "intro" +page_title: "Build Infrastructure" +sidebar_current: "gettingstarted-build" +--- + +# Build Infrastructure + +With Terraform installed, let's dive right into it and start creating +some infrastructure. + +We'll build infrastructure on +[AWS](http://aws.amazon.com) for the getting started guide +since it is popular and generally understood, but Terraform +can [manage many providers](#), +including multiple providers in a single configuration. +Some examples of this are in the +[use cases section](/intro/use-cases.html). + +If you don't have an AWS account, +[create one now](http://aws.amazon.com/free/). +For the getting started guide, we'll only be using resources +which qualify under the AWS +[free-tier](http://aws.amazon.com/free/), +meaning it will be free. +If you already have an AWS account, you may be charged some +amount of money, but it shouldn't be more than a few dollars +at most. + +
+

+Note: If you're not using an account that qualifies +under the AWS +free-tier, +you may be charged to run these examples. The most you should +be charged should only be a few dollars, but we're not responsible +for any charges that may incur. +

+
+ +## Configuration + +The set of files used to describe infrastructure in Terraform is simply +known as a Terraform _configuration_. We're going to write our first +configuration now to launch a single AWS EC2 instance. + +The format of the configuration files is +[documented here](#). +Configuration files can +[also be JSON](#), but we recommend only using JSON when the +configuration is generated by a machine. + +The entire configuration is shown below. We'll go over each part +after. Save the contents to a file named `example.tf`. Verify that +there are no other `*.tf` files in your directory, since Terraform +loads all of them. + +``` +provider "aws" { + access_key = "ACCESS_KEY_HERE" + secret_key = "SECRET_KEY_HERE" + region = "us-east-1" +} + +resource "aws_instance" "example" { + ami = "ami-408c7f28" + instance_type = "t1.micro" +} +``` + +Replace the `ACCESS_KEY_HERE` and `SECRET_KEY_HERE` with your +AWS access key and secret key, available from +[this page](https://console.aws.amazon.com/iam/home?#security_credential). +We're hardcoding them for now, but will extract these into +variables later in the getting started guide. + +This is a complete configuration that Terraform is ready to apply. +The general structure should be intuitive and straightforward. + +The `provider` block is used to configure the named provider, in +our case "aws." A provider is responsible for creating and +managing resources. Multiple provider blocks can exist if a +Terraform configuration is comprised of multiple providers, +which is a common situation. + +The `resource` block defines a resource that exists within +the infrastructure. A resource might be a physical component such +as an EC2 instance, or it can be a logical resource such as +a Heroku applcation. + +The resource block has two strings before opening the block: +the resource type and the resource name. In our example, the +resource type is "aws\_instance" and the name is "example." +The prefix of the type maps to the provider. In our case +"aws\_instance" automatically tells Terraform that it is +managed by the "aws" provider. + +Within the resource block itself is configuration for that +resource. This is dependent on each resource provider and +is fully documented within our +[providers reference](#). For our EC2 instance, we specify +an AMI for Ubuntu, and request a "t1.micro" instance so we +qualify under the free tier. + +## Execution Plan + +Next, let's see what Terraform would do if we asked it to +apply this configuration. In the same directory as the +`example.tf` file you created, run `terraform plan`. You +should see output similar to what is copied below. We've +truncated some of the output to save space. + +``` +$ terraform plan +... + ++ aws_instance.example + ami: "" => "ami-408c7f28" + availability_zone: "" => "" + instance_type: "" => "t1.micro" + key_name: "" => "" + private_dns: "" => "" + private_ip: "" => "" + public_dns: "" => "" + public_ip: "" => "" + security_groups: "" => "" + subnet_id: "" => "" +``` + +`terraform plan` shows what changes Terraform will apply to +your infrastructure given the current state of your infrastructure +as well as the current contents of your configuration. + +If `terraform plan` failed with an error, read the error message +and fix the error that occurred. At this stage, it is probably a +syntax error in the configuration. + +The output format is similar to the diff format generated by tools +such as Git. The output has a "+" next to "aws\_instance.example", +meaning that Terraform will create this resource. Beneath that, +it shows the attributes that will be set. When the value it is +going to is ``, it means that the value won't be known +until the resource is created. + +## Apply + +The plan looks good, our configuration appears valid, so its time to +create real resources. Run `terraform apply` in the same directory +as your `example.tf`, and watch it go! It will take a few minutes +since Terraform waits for the EC2 instance to become available. + +``` +$ terraform apply +aws_instance.example: Creating... + ami: "" => "ami-408c7f28" + instance_type: "" => "t1.micro" + +Apply complete! Resources: 1 added, 0 changed, 0 destroyed. + +... +``` + +Done! You can go to the AWS console to prove to yourself that the +EC2 instance has been created. + +Terraform also put some state into the `terraform.tfstate` file +by default. This state file is extremely important; it maps various +resource metadata to actual resource IDs so that Terraform knows +what it is managing. This file must be saved and distributed +to anyone who might run Terraform. We recommend simply putting it +into version control, since it generally isn't too large. + +You can inspect the state using `terraform show`: + +``` +$ terraform show +aws_instance.example: + id = i-e60900cd + ami = ami-408c7f28 + availability_zone = us-east-1c + instance_type = t1.micro + key_name = + private_dns = domU-12-31-39-12-38-AB.compute-1.internal + private_ip = 10.200.59.89 + public_dns = ec2-54-81-21-192.compute-1.amazonaws.com + public_ip = 54.81.21.192 + security_groups.# = 1 + security_groups.0 = default + subnet_id = +``` + +You can see that by creating our resource, we've also gathered +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. + +## Next + +Congratulations! You've built your first infrastructure with Terraform. +You've seen the configuration syntax, an example of a basic execution +plan, and understand the state file. + +Next, we're going to move on to changing and destroying infrastructure. diff --git a/website/source/intro/getting-started/change.html.md b/website/source/intro/getting-started/change.html.md new file mode 100644 index 000000000..d9a4243ea --- /dev/null +++ b/website/source/intro/getting-started/change.html.md @@ -0,0 +1,95 @@ +--- +layout: "intro" +page_title: "Change Infrastructure" +sidebar_current: "gettingstarted-change" +--- + +# Change Infrastructure + +In the previous page, you created your first infrastructure with +Terraform: a single EC2 instance. In this page, we're going to +modify that resource, and see how Terraform handles change. + +Infrastructure is continuously evolving, and Terraform was built +to help manage and enact that change. As you change Terraform +configurations, Terraform builds an execution plan that only +modifies what is necessary to reach your desired state. + +By using Terraform to change infrastructure, you can version +control not only your configurations but also your state so you +can see how the infrastructure evolved over time. + +## Configuration + +Let's modify the `ami` of our instance. Edit the "aws\_instance.web" +resource in your configuration and change it to the following: + +``` +resource "aws_instance" "example" { + ami = "ami-aa7ab6c2" + instance_type = "t1.micro" +} +``` + +We've changed the AMI from being an Ubuntu 14.04 AMI to being +an Ubuntu 12.04 AMI. Terraform configurations are meant to be +changed like this. You can also completely remove resources +and Terraform will know to destroy the old one. + +## Execution Plan + +Let's see what Terraform will do with the change we made. + +``` +$ terraform plan +... + +-/+ aws_instance.example + ami: "ami-408c7f28" => "ami-aa7ab6c2" (forces new resource) + availability_zone: "us-east-1c" => "" + key_name: "" => "" + private_dns: "domU-12-31-39-12-38-AB.compute-1.internal" => "" + private_ip: "10.200.59.89" => "" + public_dns: "ec2-54-81-21-192.compute-1.amazonaws.com" => "" + public_ip: "54.81.21.192" => "" + security_groups: "" => "" + subnet_id: "" => "" +``` + +The prefix "-/+" means that Terraform will destroy and recreate +the resource, versus purely updating it in-place. While some attributes +can do in-place updates (which are shown with a "~" prefix), AMI +changing on EC2 instance requires a new resource. Terraform handles +these details for you, and the execution plan makes it clear what +Terraform will do. + +Additionally, the plan output shows that the AMI change is what +necessitated the creation of a new resource. Using this information, +you can tweak your changes to possibly avoid destroy/create updates +if you didn't want to do them at this time. + +## Apply + +From the plan, we know what will happen. Let's apply and enact +the change. + +``` +$ terraform apply +aws_instance.example: Destroying... +aws_instance.example: Modifying... + ami: "ami-408c7f28" => "ami-aa7ab6c2" + +Apply complete! Resources: 0 added, 1 changed, 1 destroyed. + +... +``` + +As the plan predicted, Terraform started by destroying our old +instance, then creating the new one. You can use `terraform show` +again to see the new properties associated with this instance. + +## Next + +You've now seen how easy it is to modify infrastructure with +Terraform. Feel free to play around with this more before continuing. +In the next section we're going to destroy our infrastructure. diff --git a/website/source/intro/getting-started/checks.html.markdown b/website/source/intro/getting-started/checks.html.markdown deleted file mode 100644 index 81b2fddd0..000000000 --- a/website/source/intro/getting-started/checks.html.markdown +++ /dev/null @@ -1,94 +0,0 @@ ---- -layout: "intro" -page_title: "Registering Health Checks" -sidebar_current: "gettingstarted-checks" ---- - -# Health Checks - -We've now seen how simple it is to run Terraform, add nodes and services, and -query those nodes and services. In this section we will continue by adding -health checks to both nodes and services, a critical component of service -discovery that prevents using services that are unhealthy. - -This page will build upon the previous page and assumes you have a -two node cluster running. - -## Defining Checks - -Similarly to a service, a check can be registered either by providing a -[check definition](/docs/agent/checks.html) -, or by making the appropriate calls to the -[HTTP API](/docs/agent/http.html). - -We will use the check definition, because just like services, definitions -are the most common way to setup checks. - -Create two definition files in the Terraform configuration directory of -the second node. -The first file will add a host-level check, and the second will modify the web -service definition to add a service-level check. - -``` -$ echo '{"check": {"name": "ping", "script": "ping -c1 google.com >/dev/null", "interval": "30s"}}' >/etc/terraform.d/ping.json - -$ echo '{"service": {"name": "web", "tags": ["rails"], "port": 80, - "check": {"script": "curl localhost:80 >/dev/null 2>&1", "interval": "10s"}}}' >/etc/terraform.d/web.json -``` - -The first definition adds a host-level check named "ping". This check runs -on a 30 second interval, invoking `ping -c1 google.com`. If the command -exits with a non-zero exit code, then the node will be flagged unhealthy. - -The second command modifies the web service and adds a check that uses -curl every 10 seconds to verify that the web server is running. - -Restart the second agent, or send a `SIGHUP` to it. We should now see the -following log lines: - -``` -==> Starting Terraform agent... -... - [INFO] agent: Synced service 'web' - [INFO] agent: Synced check 'service:web' - [INFO] agent: Synced check 'ping' - [WARN] Check 'service:web' is now critical -``` - -The first few log lines indicate that the agent has synced the new -definitions. The last line indicates that the check we added for -the `web` service is critical. This is because we're not actually running -a web server and the curl test is failing! - -## Checking Health Status - -Now that we've added some simple checks, we can use the HTTP API to check -them. First, we can look for any failing checks. You can run this curl -on either node: - -``` -$ curl http://localhost:8500/v1/health/state/critical -[{"Node":"agent-two","CheckID":"service:web","Name":"Service 'web' check","Status":"critical","Notes":"","ServiceID":"web","ServiceName":"web"}] -``` - -We can see that there is only a single check in the `critical` state, which is -our `web` service check. - -Additionally, we can attempt to query the web service using DNS. Terraform -will not return any results, since the service is unhealthy: - -``` - dig @127.0.0.1 -p 8600 web.service.terraform -... - -;; QUESTION SECTION: -;web.service.terraform. IN A -``` - -This section should have shown that checks can be easily added. Check definitions -can be updated by changing configuration files and sending a `SIGHUP` to the agent. -Alternatively the HTTP API can be used to add, remove and modify checks dynamically. -The API allows for a "dead man's switch" or [TTL based check](/docs/agent/checks.html). -TTL checks can be used to integrate an application more tightly with Terraform, enabling -business logic to be evaluated as part of passing a check. - diff --git a/website/source/intro/getting-started/destroy.html.md b/website/source/intro/getting-started/destroy.html.md new file mode 100644 index 000000000..1f4ff9a70 --- /dev/null +++ b/website/source/intro/getting-started/destroy.html.md @@ -0,0 +1,68 @@ +--- +layout: "intro" +page_title: "Destroy Infrastructure" +sidebar_current: "gettingstarted-destroy" +--- + +# Destroy Infrastructure + +We've now seen how to build and change infrastructure. Before we +move on to creating multiple resources and showing resource +dependencies, we're going to go over how to completely destroy +the Terraform-managed infrastructure. + +Destroying your infrastructure is a rare event in production +environments. But if you're using Terraform to spin up multiple +environments such as development, test, QA environments, then +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. + +``` +$ terraform plan -destroy +... + +- aws_instance.example +``` + +The output says that "aws\_instance.example" will be deleted. + +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. + +## Apply + +Let's apply the destroy: + +``` +$ terraform apply -destroy +aws_instance.example: Destroying... + +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. + +## Next + +You now know how to create, modify, and destroy infrastructure. +With these building blocks, you can effectively experiment with +any part of Terraform. + +Next, we move on to features that make Terraform configurations +slightly more useful: variables, resource dependencies, provisioning, +and more. diff --git a/website/source/intro/getting-started/install.html.markdown b/website/source/intro/getting-started/install.html.markdown index cfd271daf..e5ff20512 100644 --- a/website/source/intro/getting-started/install.html.markdown +++ b/website/source/intro/getting-started/install.html.markdown @@ -6,36 +6,25 @@ sidebar_current: "gettingstarted-install" # Install Terraform -Terraform must first be installed on every node that will be a member of a -Terraform cluster. To make installation easy, Terraform is distributed as a -[binary package](/downloads.html) for all supported platforms and -architectures. This page will not cover how to compile Terraform from +Terraform must first be installed on your machine. Terraform is distributed +as a [binary package](/downloads.html) for all supported platforms and +architecture. This page will not cover how to compile Terraform from source. ## Installing Terraform To install Terraform, find the [appropriate package](/downloads.html) for -your system and download it. Terraform is packaged as a "zip" archive. +your system and download it. Terraform is packaged as a zip archive. -After downloading Terraform, unzip the package. Copy the `terraform` binary to -somewhere on the PATH so that it can be executed. On Unix systems, -`~/bin` and `/usr/local/bin` are common installation directories, -depending on if you want to restrict the install to a single user or -expose it to the entire system. On Windows systems, you can put it wherever -you would like. - -### OS X - -If you are using [homebrew](http://brew.sh/#install) as a package manager, -than you can install terraform as simple as: -``` -brew cask install terraform -``` - -if you are missing the [cask plugin](http://caskroom.io/) you can install it with: -``` -brew install caskroom/cask/brew-cask -``` +After downloading Terraform, unzip the package into a directory where +Terraform will be installed. The directory will contain a set of binary +programs, such as `terraform`, `terraform-provider-aws`, etc. The final +step is to make sure the directory you installed Terraform to is on the +PATH. See +[this page](http://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux) +for instructions on setting the PATH on Linux and Mac. +[This page](http://stackoverflow.com/questions/1618280/where-can-i-set-path-to-make-exe-on-windows) +contains instructions for setting the PATH on Windows. ## Verifying the Installation @@ -48,15 +37,13 @@ $ terraform usage: terraform [--version] [--help] [] Available commands are: - agent Runs a Terraform agent - force-leave Forces a member of the cluster to enter the "left" state - info Provides debugging information for operators - join Tell Terraform agent to join cluster - keygen Generates a new encryption key - leave Gracefully leaves the Terraform cluster and shuts down - members Lists the members of a Terraform cluster - monitor Stream logs from a Terraform agent - version Prints the Terraform version + 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 ``` If you get an error that `terraform` could not be found, then your PATH diff --git a/website/source/intro/getting-started/join.html.markdown b/website/source/intro/getting-started/join.html.markdown deleted file mode 100644 index fc710f689..000000000 --- a/website/source/intro/getting-started/join.html.markdown +++ /dev/null @@ -1,121 +0,0 @@ ---- -layout: "intro" -page_title: "Terraform Cluster" -sidebar_current: "gettingstarted-join" ---- - -# Terraform Cluster - -By this point, we've started our first agent and registered and queried -one or more services on that agent. This showed how easy it is to use -Terraform, but didn't show how this could be extended to a scalable production -service discovery infrastructure. On this page, we'll create our first -real cluster with multiple members. - -When starting a Terraform agent, it begins without knowledge of any other node, and is -an isolated cluster of one. To learn about other cluster members, the agent must -_join_ an existing cluster. To join an existing cluster, only needs to know -about a _single_ existing member. After it joins, the agent will gossip with this -member and quickly discover the other members in the cluster. A Terraform -agent can join any other agent, it doesn't have to be an agent in server mode. - -## Starting the Agents - -To simulate a more realistic cluster, we are using a two node cluster in -Vagrant. The Vagrantfile can be found in the demo section of the repo -[here](https://github.com/hashicorp/terraform/tree/master/demo/vagrant-cluster). - -We start the first agent on our first node and also specify a node name. -The node name must be unique and is how a machine is uniquely identified. -By default it is the hostname of the machine, but we'll manually override it. -We are also providing a bind address. This is the address that Terraform listens on, -and it *must* be accessible by all other nodes in the cluster. The first node -will act as our server in this cluster. We're still not making a cluster -of servers. - -``` -$ terraform agent -server -bootstrap -data-dir /tmp/consul \ - -node=agent-one -bind=172.20.20.10 -... -``` - -Then, in another terminal, start the second agent on the new node. -This time, we set the bind address to match the IP of the second node -as specified in the Vagrantfile. In production, you will generally want -to provide a bind address or interface as well. - -``` -$ terraform agent -data-dir /tmp/consul -node=agent-two -bind=172.20.20.11 -... -``` - -At this point, you have two Terraform agents running, one server and one client. -The two Terraform agents still don't know anything about each other, and are each part of their own -clusters (of one member). You can verify this by running `terraform members` -against each agent and noting that only one member is a part of each. - -## Joining a Cluster - -Now, let's tell the first agent to join the second agent by running -the following command in a new terminal: - -``` -$ terraform join 172.20.20.11 -Successfully joined cluster by contacting 1 nodes. -``` - -You should see some log output in each of the agent logs. If you read -carefully, you'll see that they received join information. If you -run `terraform members` against each agent, you'll see that both agents now -know about each other: - -``` -$ terraform members -agent-one 172.20.20.10:8301 alive role=terraform,dc=dc1,vsn=1,vsn_min=1,vsn_max=1,port=8300,bootstrap=1 -agent-two 172.20.20.11:8301 alive role=node,dc=dc1,vsn=1,vsn_min=1,vsn_max=1 -``` - -
-

Remember: To join a cluster, a Terraform agent needs to only -learn about one existing member. After joining the cluster, the -agents gossip with each other to propagate full membership information. -

-
- -In addition to using `terraform join` you can use the `-join` flag on -`terraform agent` to join a cluster as part of starting up the agent. - -## Querying Nodes - -Just like querying services, Terraform has an API for querying the -nodes themselves. You can do this via the DNS or HTTP API. - -For the DNS API, the structure of the names is `NAME.node.terraform` or -`NAME.DATACENTER.node.terraform`. If the datacenter is omitted, Terraform -will only search the local datacenter. - -From "agent-one", query "agent-two": - -``` -$ dig @127.0.0.1 -p 8600 agent-two.node.terraform -... - -;; QUESTION SECTION: -;agent-two.node.terraform. IN A - -;; ANSWER SECTION: -agent-two.node.terraform. 0 IN A 172.20.20.11 -``` - -The ability to look up nodes in addition to services is incredibly -useful for system administration tasks. For example, knowing the address -of the node to SSH into is as easy as making it part of the Terraform cluster -and querying it. - -## Leaving a Cluster - -To leave the cluster, you can either gracefully quit an agent (using -`Ctrl-C`) or force kill one of the agents. Gracefully leaving allows -the node to transition into the _left_ state, otherwise other nodes -will detect it as having _failed_. The difference is covered -in more detail [here](/intro/getting-started/agent.html#toc_3). diff --git a/website/source/intro/getting-started/kv.html.markdown b/website/source/intro/getting-started/kv.html.markdown deleted file mode 100644 index dd424e737..000000000 --- a/website/source/intro/getting-started/kv.html.markdown +++ /dev/null @@ -1,118 +0,0 @@ ---- -layout: "intro" -page_title: "Key/Value Data" -sidebar_current: "gettingstarted-kv" ---- - -# Key/Value Data - -In addition to providing service discovery and integrated health checking, -Terraform provides an easy to use Key/Value store. This can be used to hold -dynamic configuration, assist in service coordination, build leader election, -and anything else a developer can think to build. The -[HTTP API](/docs/agent/http.html) fully documents the features of the K/V store. - -This page assumes you have at least one Terraform agent already running. - -## Simple Usage - -To demonstrate how simple it is to get started, we will manipulate a few keys -in the K/V store. - -Querying the agent we started in a prior page, we can first verify that -there are no existing keys in the k/v store: - -``` -$ curl -v http://localhost:8500/v1/kv/?recurse -* About to connect() to localhost port 8500 (#0) -* Trying 127.0.0.1... connected -> GET /v1/kv/?recurse HTTP/1.1 -> User-Agent: curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3 -> Host: localhost:8500 -> Accept: */* -> -< HTTP/1.1 404 Not Found -< X-Terraform-Index: 1 -< Date: Fri, 11 Apr 2014 02:10:28 GMT -< Content-Length: 0 -< Content-Type: text/plain; charset=utf-8 -< -* Connection #0 to host localhost left intact -* Closing connection #0 -``` - -Since there are no keys, we get a 404 response back. -Now, we can put a few example keys: - -``` -$ curl -X PUT -d 'test' http://localhost:8500/v1/kv/web/key1 -true -$ curl -X PUT -d 'test' http://localhost:8500/v1/kv/web/key2?flags=42 -true -$ curl -X PUT -d 'test' http://localhost:8500/v1/kv/web/sub/key3 -true -$ curl http://localhost:8500/v1/kv/?recurse -[{"CreateIndex":97,"ModifyIndex":97,"Key":"web/key1","Flags":0,"Value":"dGVzdA=="}, - {"CreateIndex":98,"ModifyIndex":98,"Key":"web/key2","Flags":42,"Value":"dGVzdA=="}, - {"CreateIndex":99,"ModifyIndex":99,"Key":"web/sub/key3","Flags":0,"Value":"dGVzdA=="}] -``` - -Here we have created 3 keys, each with the value of "test". Note that the -`Value` field returned is base64 encoded to allow non-UTF8 -characters. For the "web/key2" key, we set a `flag` value of 42. All keys -support setting a 64bit integer flag value. This is opaque to Terraform but can -be used by clients for any purpose. - -After setting the values, we then issued a GET request to retrieve multiple -keys using the `?recurse` parameter. - -You can also fetch a single key just as easily: - -``` -$ curl http://localhost:8500/v1/kv/web/key1 -[{"CreateIndex":97,"ModifyIndex":97,"Key":"web/key1","Flags":0,"Value":"dGVzdA=="}] -``` - -Deleting keys is simple as well. We can delete a single key by specifying the -full path, or we can recursively delete all keys under a root using "?recurse": - -``` -$ curl -X DELETE http://localhost:8500/v1/kv/web/sub?recurse -$ curl http://localhost:8500/v1/kv/web?recurse -[{"CreateIndex":97,"ModifyIndex":97,"Key":"web/key1","Flags":0,"Value":"dGVzdA=="}, - {"CreateIndex":98,"ModifyIndex":98,"Key":"web/key2","Flags":42,"Value":"dGVzdA=="}] -``` - -A key can be updated by setting a new value by issuing the same PUT request. -Additionally, Terraform provides a Check-And-Set operation, enabling atomic -key updates. This is done by providing the `?cas=` paramter with the last -`ModifyIndex` value from the GET request. For example, suppose we wanted -to update "web/key1": - -``` -$ curl -X PUT -d 'newval' http://localhost:8500/v1/kv/web/key1?cas=97 -true -$ curl -X PUT -d 'newval' http://localhost:8500/v1/kv/web/key1?cas=97 -false -``` - -In this case, the first CAS update succeeds because the last modify time is 97. -However the second operation fails because the `ModifyIndex` is no longer 97. - -We can also make use of the `ModifyIndex` to wait for a key's value to change. -For example, suppose we wanted to wait for key2 to be modified: - -``` -$ curl "http://localhost:8500/v1/kv/web/key2?index=101&wait=5s" -[{"CreateIndex":98,"ModifyIndex":101,"Key":"web/key2","Flags":42,"Value":"dGVzdA=="}] -``` - -By providing "?index=" we are asking to wait until the key has a `ModifyIndex` greater -than 101. However the "?wait=5s" parameter restricts the query to at most 5 seconds, -returning the current, unchanged value. This can be used to efficiently wait for -key modifications. Additionally, this same technique can be used to wait for a list -of keys, waiting only until any of the keys has a newer modification time. - -This is only a few example of what the API supports. For full documentation, please -reference the [HTTP API](/docs/agent/http.html). - diff --git a/website/source/intro/getting-started/services.html.markdown b/website/source/intro/getting-started/services.html.markdown deleted file mode 100644 index dfd070863..000000000 --- a/website/source/intro/getting-started/services.html.markdown +++ /dev/null @@ -1,139 +0,0 @@ ---- -layout: "intro" -page_title: "Registering Services" -sidebar_current: "gettingstarted-services" ---- - -# Registering Services - -In the previous page, we ran our first agent, saw the cluster members, and -queried that node. On this page, we'll register our first service and query -that service. We're not yet running a cluster of Terraform agents. - -## Defining a Service - -A service can be registered either by providing a -[service definition](/docs/agent/services.html), -or by making the appropriate calls to the -[HTTP API](/docs/agent/http.html). - -We're going to start by registering a service using a service definition, -since this is the most common way that services are registered. We'll be -building on what we covered in the -[previous page](/intro/getting-started/agent.html). - -First, create a directory for Terraform configurations. A good directory -is typically `/etc/terraform.d`. Terraform loads all configuration files in the -configuration directory. - -``` -$ sudo mkdir /etc/terraform.d -``` - -Next, we'll write a service definition configuration file. We'll -pretend we have a service named "web" running on port 80. Additionally, -we'll give it some tags, which we can use as additional ways to query -it later. - -``` -$ echo '{"service": {"name": "web", "tags": ["rails"], "port": 80}}' \ - >/etc/terraform.d/web.json -``` - -Now, restart the agent we're running, providing the configuration directory: - -``` -$ terraform agent -server -bootstrap -data-dir /tmp/consul -config-dir /etc/consul.d -==> Starting Terraform agent... -... - [INFO] agent: Synced service 'web' -... -``` - -You'll notice in the output that it "synced" the web service. This means -that it loaded the information from the configuration. - -If you wanted to register multiple services, you create multiple service -definition files in the Terraform configuration directory. - -## Querying Services - -Once the agent is started and the service is synced, we can query that -service using either the DNS or HTTP API. - -### DNS API - -Let's first query it using the DNS API. For the DNS API, the DNS name -for services is `NAME.service.terraform`. All DNS names are always in the -`terraform` namespace. The `service` subdomain on that tells Terraform we're -querying services, and the `NAME` is the name of the service. For the -web service we registered, that would be `web.service.terraform`: - -``` -$ dig @127.0.0.1 -p 8600 web.service.terraform -... - -;; QUESTION SECTION: -;web.service.terraform. IN A - -;; ANSWER SECTION: -web.service.terraform. 0 IN A 172.20.20.11 -``` - -As you can see, an A record was returned with the IP address of the node that -the service is available on. A records can only hold IP addresses. You can -also use the DNS API to retrieve the entire address/port pair using SRV -records: - -``` -$ dig @127.0.0.1 -p 8600 web.service.terraform SRV -... - -;; QUESTION SECTION: -;web.service.terraform. IN SRV - -;; ANSWER SECTION: -web.service.terraform. 0 IN SRV 1 1 80 agent-one.node.dc1.consul. - -;; ADDITIONAL SECTION: -agent-one.node.dc1.terraform. 0 IN A 172.20.20.11 -``` - -The SRV record returned says that the web service is running on port 80 -and exists on the node `agent-one.node.dc1.terraform.`. An additional section -is returned by the DNS with the A record for that node. - -Finally, we can also use the DNS API to filter services by tags. The -format for tag-based service queries is `TAG.NAME.service.terraform`. In -the example below, we ask Terraform for all web services with the "rails" -tag. We get a response since we registered our service with that tag. - -``` -$ dig @127.0.0.1 -p 8600 rails.web.service.terraform -... - -;; QUESTION SECTION: -;rails.web.service.terraform. IN A - -;; ANSWER SECTION: -rails.web.service.terraform. 0 IN A 172.20.20.11 -``` - -### HTTP API - -In addition to the DNS API, the HTTP API can be used to query services: - -``` -$ curl http://localhost:8500/v1/catalog/service/web -[{"Node":"agent-one","Address":"172.20.20.11","ServiceID":"web","ServiceName":"web","ServiceTags":["rails"],"ServicePort":80}] -``` - -## Updating Services - -Service definitions can be updated by changing configuration files and -sending a `SIGHUP` to the agent. This lets you update services without -any downtime or unavailability to service queries. - -Alternatively the HTTP API can be used to add, remove, and modify services -dynamically. - diff --git a/website/source/intro/getting-started/ui.html.markdown b/website/source/intro/getting-started/ui.html.markdown deleted file mode 100644 index ab782fca4..000000000 --- a/website/source/intro/getting-started/ui.html.markdown +++ /dev/null @@ -1,56 +0,0 @@ ---- -layout: "intro" -page_title: "Web UI" -sidebar_current: "gettingstarted-ui" ---- - -# Terraform Web UI - -Terraform comes with support for a -[beautiful, functional web UI](http://demo.terraform.io) out of the box. -This UI can be used for viewing all services and nodes, viewing all -health checks and their current status, and for reading and setting -key/value data. The UI automatically supports multi-datacenter. - -For ease of deployment, the UI is -[distributed](/downloads_web_ui.html) -as static HTML and JavaScript. -You don't need a separate web server to run the web UI. The Terraform -agent itself can be configured to serve the UI. - -## Screenshot and Demo - -You can view a live demo of the Terraform Web UI -[here](http://demo.terraform.io). - -While the live demo is able to access data from all datacenters, -we've also setup demo endpoints in the specific datacenters: -[AMS2](http://ams2.demo.terraform.io) (Amsterdam), -[SFO1](http://sfo1.demo.terraform.io) (San Francisco), -and [NYC1](http://nyc1.demo.terraform.io) (New York). - -A screenshot of one page of the demo is shown below so you can get an -idea of what the web UI is like. Click the screenshot for the full size. - -
- - - -
- -## Set Up - -To set up the web UI, -[download the web UI package](/downloads_web_ui.html) -and unzip it to a directory somewhere on the server where the Terraform agent -is also being run. Then, just append the `-ui-dir` to the `terraform agent` -command pointing to the directory where you unzipped the UI (the -directory with the `index.html` file): - -``` -$ terraform agent -ui-dir /path/to/ui -... -``` - -The UI is available at the `/ui` path on the same port as the HTTP API. -By default this is `http://localhost:8500/ui`. diff --git a/website/source/intro/index.html.markdown b/website/source/intro/index.html.markdown index bc47ea037..847ab875d 100644 --- a/website/source/intro/index.html.markdown +++ b/website/source/intro/index.html.markdown @@ -6,70 +6,66 @@ sidebar_current: "what" # Introduction to Terraform -Welcome to the intro guide to Terraform! This guide is the best place to start -with Terraform. We cover what Terraform is, what problems it can solve, how it compares -to existing software, and a quick start for using Terraform. If you are already familiar -with the basics of Terraform, the [documentation](/docs/index.html) provides more -of a reference for all available features. +Welcome to the intro guide to Terraform! This guide is the best +place to start with Terraform. We cover what Terraform is, what +problems it can solve, how it compares to existing software, +and contains a quick start for using Terraform. + +If you are already familiar with the basics of Terraform, the +[documentation](/docs/index.html) provides a better reference +guide for all available features as well as internals. ## What is Terraform? -Terraform has multiple components, but as a whole, it is a tool for discovering -and configuring services in your infrastructure. It provides several -key features: +Terraform is a tool for building, changing, and versioning infrastructure +safely and efficiently. Terraform can manage existing and popular service +providers as well as custom in-house solutions. -* **Service Discovery**: Clients of Terraform can _provide_ a service, such as - `api` or `mysql`, and other clients can use Terraform to _discover_ providers - of a given service. Using either DNS or HTTP, applications can easily find - the services they depend upon. +Configuration files describe to Terraform the components needed to +run a single application or your entire datacenter. +Terraform generates an execution plan describing +what it will do to reach the desired state, and then executes it to build the +described infrastructure. As the configuration changes, Terraform is able +to determine what changed and create incremental execution plans which +can be applied. -* **Health Checking**: Terraform clients can provide any number of health checks, - either associated with a given service ("is the webserver returning 200 OK"), or - with the local node ("is memory utilization below 90%"). This information can be - used by an operator to monitor cluster health, and it is used by the service - discovery components to route traffic away from unhealthy hosts. +The infrastructure Terraform can manage includes +low-level components such as +compute instances, storage, and networking, as well as high-level +components such as DNS entries, SaaS features, etc. -* **Key/Value Store**: Applications can make use of Terraform's hierarchical key/value - store for any number of purposes including: dynamic configuration, feature flagging, - coordination, leader election, etc. The simple HTTP API makes it easy to use. +Examples work best to showcase Terraform. Please see the +[use cases](/intro/use-cases.html). -* **Multi Datacenter**: Terraform supports multiple datacenters out of the box. This - means users of Terraform do not have to worry about building additional layers of - abstraction to grow to multiple regions. +The key features of Terraform are: -Terraform is designed to be friendly to both the DevOps community and -application developers, making it perfect for modern, elastic infrastructures. +* **Infrastructure as Code**: Infrastructure is described using a high-level + configuration syntax. This allows a blueprint of your datacenter to be + versioned and treated as you would any other code. Additionally, + infrastructure can be shared and re-used. -## Basic Architecture of Terraform +* **Execution Plans**: Terraform has a "planning" step where it generates + an _execution plan_. The execution plan shows what Terraform will do when + you call apply. This lets you avoid any surprises when Terraform + manipulates infrastructure. -Terraform is a distributed, highly available system. There is an -[in-depth architecture overview](/docs/internals/architecture.html) available, -but this section will cover the basics so you can get an understanding -of how Terraform works. This section will purposely omit details to quickly -provide an overview of the architecture. +* **Resource Graph**: Terraform builds a graph of all your resources, + and parallelizes the creation and modification of any non-dependent + resources. Because of this, Terraform builds infrastructure as efficiently + as possible, and operators get insight into dependencies in their + infrastructure. -Every node that provides services to Terraform runs a _Terraform agent_. Running -an agent is not required for discovering other services or getting/setting -key/value data. The agent is responsible for health checking the services -on the node as well as the node itself. - -The agents talk to one or more _Terraform servers_. The Terraform servers are -where data is stored and replicated. The servers themselves elect a leader. -While Terraform can function with one server, 3 to 5 is recommended to avoid -data loss scenarios. A cluster of Terraform servers is recommended for each -datacenter. - -Components of your infrastructure that need to discover other services -or nodes can query any of the Terraform servers _or_ any of the Terraform agents. -The agents forward queries to the servers automatically. - -Each datacenter runs a cluster of Terraform servers. When a cross-datacenter -service discovery or configuration request is made, the local Terraform servers -forward the request to the remote datacenter and return the result. +* **Change Automation**: Potentially complex changesets are applied to + your infrastructure with minimal human interaction. + With the previously mentioned execution + plan and resource graph, you know exactly what Terraform will change + and in what order, avoiding many possible human errors. ## Next Steps -See the page on [how Terraform compares to other software](/intro/vs/index.html) -to see how it fits into your existing infrastructure. Or continue onwards with +See the page on [Terraform use cases](/intro/use-cases.html) to see the +multiple ways Terraform can be used. Then see +[how Terraform compares to other software](/intro/vs/index.html) +to see how it fits into your existing infrastructure. Finally, continue onwards with the [getting started guide](/intro/getting-started/install.html) to get -Terraform up and running and see how it works. +Terraform managing some real infrastructure and to see how it works. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb new file mode 100644 index 000000000..eb25b366f --- /dev/null +++ b/website/source/layouts/aws.erb @@ -0,0 +1,70 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> + <% end %> diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index c51102838..b0a276c2f 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -4,52 +4,16 @@ + > - Terraform Commands (CLI) + Commands (CLI) - > - Terraform Agent - + > + Internals + +
<% end %> diff --git a/website/source/layouts/intro.erb b/website/source/layouts/intro.erb index 41c536ffd..664b0e01b 100644 --- a/website/source/layouts/intro.erb +++ b/website/source/layouts/intro.erb @@ -4,34 +4,18 @@ diff --git a/website/source/stylesheets/_docs.less b/website/source/stylesheets/_docs.less index e06e6e8ab..ba689fbdb 100755 --- a/website/source/stylesheets/_docs.less +++ b/website/source/stylesheets/_docs.less @@ -6,7 +6,9 @@ body.page-sub{ background-color: @light-black; } +body.layout-aws, body.layout-docs, +body.layout-inner, body.layout-intro{ background: @light-black url('../images/sidebar-wire.png') left 62px no-repeat; @@ -127,6 +129,10 @@ body.layout-intro{ } } } + + .nav-visible { + display: block; + } } } @@ -192,6 +198,7 @@ body.layout-intro{ @media (max-width: 992px) { body.layout-docs, + body.layout-inner, body.layout-intro{ >.container{ .col-md-8[role=main]{ diff --git a/website/source/stylesheets/main.css b/website/source/stylesheets/main.css index dd5cc39ea..e54cd4388 100644 --- a/website/source/stylesheets/main.css +++ b/website/source/stylesheets/main.css @@ -993,16 +993,22 @@ body.page-home #footer { body.page-sub { background-color: #242424; } +body.layout-aws, body.layout-docs, +body.layout-inner, body.layout-intro { background: #242424 url('../images/sidebar-wire.png') left 62px no-repeat; } +body.layout-aws > .container .col-md-8[role=main], body.layout-docs > .container .col-md-8[role=main], +body.layout-inner > .container .col-md-8[role=main], body.layout-intro > .container .col-md-8[role=main] { min-height: 800px; background-color: white; } +body.layout-aws > .container .col-md-8[role=main] > div, body.layout-docs > .container .col-md-8[role=main] > div, +body.layout-inner > .container .col-md-8[role=main] > div, body.layout-intro > .container .col-md-8[role=main] > div { position: relative; z-index: 10; @@ -1096,6 +1102,9 @@ body.layout-intro > .container .col-md-8[role=main] > div { -webkit-font-smoothing: antialiased; padding: 6px 15px; } +.docs-sidebar .docs-sidenav .nav-visible { + display: block; +} .bs-docs-section { padding-top: 10px; padding-left: 3%; @@ -1151,10 +1160,12 @@ body.layout-intro > .container .col-md-8[role=main] > div { } @media (max-width: 992px) { body.layout-docs > .container .col-md-8[role=main], + body.layout-inner > .container .col-md-8[role=main], body.layout-intro > .container .col-md-8[role=main] { min-height: 0; } body.layout-docs > .container .col-md-8[role=main]::before, + body.layout-inner > .container .col-md-8[role=main]::before, body.layout-intro > .container .col-md-8[role=main]::before { border-left: 9999px solid white; } diff --git a/website/source/stylesheets/main.less b/website/source/stylesheets/main.less index f542f49af..98e699d7a 100755 --- a/website/source/stylesheets/main.less +++ b/website/source/stylesheets/main.less @@ -26,4 +26,4 @@ // Components w/ JavaScript /*@import "modals.less";*/ -// 68 +// 69