From 09f5935993d05c11b589b8fcdd709d833463dde3 Mon Sep 17 00:00:00 2001 From: Tomas Doran Date: Fri, 27 Feb 2015 10:15:12 -0800 Subject: [PATCH 01/59] Allow launch configuration names to be computed This allows you to set lifecycle create_before_destroy = true and fixes #532 as then we'll make a new launch config, change the launch config on the ASG, and *then* delete the old launch config. Also tried adding tests which unfortunately don't seem to fail... --- .../aws/resource_aws_launch_configuration.go | 15 +++++++++++++-- .../resource_aws_launch_configuration_test.go | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/builtin/providers/aws/resource_aws_launch_configuration.go b/builtin/providers/aws/resource_aws_launch_configuration.go index e6b2f37425f8..84edd9fd4f3b 100644 --- a/builtin/providers/aws/resource_aws_launch_configuration.go +++ b/builtin/providers/aws/resource_aws_launch_configuration.go @@ -24,7 +24,8 @@ func resourceAwsLaunchConfiguration() *schema.Resource { Schema: map[string]*schema.Schema{ "name": &schema.Schema{ Type: schema.TypeString, - Required: true, + Optional: true, + Computed: true, ForceNew: true, }, @@ -122,13 +123,23 @@ func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface v.(*schema.Set).List()) } + if v, ok := d.GetOk("name"); ok { + createLaunchConfigurationOpts.LaunchConfigurationName = aws.String(v.(string)) + d.SetId(d.Get("name").(string)) + } else { + hash := sha1.Sum([]byte(fmt.Sprintf("%#v", createLaunchConfigurationOpts))) + config_name := fmt.Sprintf("terraform-%s", base64.URLEncoding.EncodeToString(hash[:])) + log.Printf("[DEBUG] Computed Launch config name: %s", config_name) + createLaunchConfigurationOpts.LaunchConfigurationName = aws.String(config_name) + d.SetId(config_name) + } + log.Printf("[DEBUG] autoscaling create launch configuration: %#v", createLaunchConfigurationOpts) err := autoscalingconn.CreateLaunchConfiguration(&createLaunchConfigurationOpts) if err != nil { return fmt.Errorf("Error creating launch configuration: %s", err) } - d.SetId(d.Get("name").(string)) log.Printf("[INFO] launch configuration ID: %s", d.Id()) // We put a Retry here since sometimes eventual consistency bites diff --git a/builtin/providers/aws/resource_aws_launch_configuration_test.go b/builtin/providers/aws/resource_aws_launch_configuration_test.go index 500d3ca07b43..eb557f08e61f 100644 --- a/builtin/providers/aws/resource_aws_launch_configuration_test.go +++ b/builtin/providers/aws/resource_aws_launch_configuration_test.go @@ -45,6 +45,16 @@ func TestAccAWSLaunchConfiguration(t *testing.T) { "aws_launch_configuration.bar", "spot_price", "0.01"), ), }, + + resource.TestStep{ + Config: testAccAWSLaunchConfigurationNoNameConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLaunchConfigurationExists("aws_launch_configuration.bar", &conf), + testAccCheckAWSLaunchConfigurationAttributes(&conf), + resource.TestCheckResourceAttr( + "aws_launch_configuration.bar", "name", "terraform-foo"), // FIXME - This should fail?!?!? + ), + }, }, }) } @@ -153,3 +163,12 @@ resource "aws_launch_configuration" "bar" { spot_price = "0.01" } ` + +const testAccAWSLaunchConfigurationNoNameConfig = ` +resource "aws_launch_configuration" "bar" { + image_id = "ami-21f78e12" + instance_type = "t1.micro" + user_data = "foobar-user-data-change" + associate_public_ip_address = false +} +` From 7632458ad3beef3ef2b15522e66a54b19ba1bdd8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Mar 2015 18:17:47 -0500 Subject: [PATCH 02/59] providers/terraform: remote state resource --- builtin/bins/provider-terraform/main.go | 12 ++++ builtin/bins/provider-terraform/main_test.go | 1 + builtin/providers/terraform/provider.go | 15 ++++ builtin/providers/terraform/provider_test.go | 31 ++++++++ builtin/providers/terraform/resource_state.go | 71 +++++++++++++++++++ .../terraform/resource_state_test.go | 54 ++++++++++++++ 6 files changed, 184 insertions(+) create mode 100644 builtin/bins/provider-terraform/main.go create mode 100644 builtin/bins/provider-terraform/main_test.go create mode 100644 builtin/providers/terraform/provider.go create mode 100644 builtin/providers/terraform/provider_test.go create mode 100644 builtin/providers/terraform/resource_state.go create mode 100644 builtin/providers/terraform/resource_state_test.go diff --git a/builtin/bins/provider-terraform/main.go b/builtin/bins/provider-terraform/main.go new file mode 100644 index 000000000000..21f4da5d2627 --- /dev/null +++ b/builtin/bins/provider-terraform/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/terraform" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: terraform.Provider, + }) +} diff --git a/builtin/bins/provider-terraform/main_test.go b/builtin/bins/provider-terraform/main_test.go new file mode 100644 index 000000000000..06ab7d0f9a35 --- /dev/null +++ b/builtin/bins/provider-terraform/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/terraform/provider.go b/builtin/providers/terraform/provider.go new file mode 100644 index 000000000000..0330ce775307 --- /dev/null +++ b/builtin/providers/terraform/provider.go @@ -0,0 +1,15 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +// Provider returns a terraform.ResourceProvider. +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "terraform_state": resourceState(), + }, + } +} diff --git a/builtin/providers/terraform/provider_test.go b/builtin/providers/terraform/provider_test.go new file mode 100644 index 000000000000..65f3ce4adb6c --- /dev/null +++ b/builtin/providers/terraform/provider_test.go @@ -0,0 +1,31 @@ +package terraform + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "terraform": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { +} diff --git a/builtin/providers/terraform/resource_state.go b/builtin/providers/terraform/resource_state.go new file mode 100644 index 000000000000..85de3990d65d --- /dev/null +++ b/builtin/providers/terraform/resource_state.go @@ -0,0 +1,71 @@ +package terraform + +import ( + "log" + "time" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/state/remote" +) + +func resourceState() *schema.Resource { + return &schema.Resource{ + Create: resourceStateCreate, + Read: resourceStateRead, + Delete: resourceStateDelete, + + Schema: map[string]*schema.Schema{ + "backend": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "config": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + + "output": &schema.Schema{ + Type: schema.TypeMap, + Computed: true, + }, + }, + } +} + +func resourceStateCreate(d *schema.ResourceData, meta interface{}) error { + return resourceStateRead(d, meta) +} + +func resourceStateRead(d *schema.ResourceData, meta interface{}) error { + backend := d.Get("backend").(string) + config := make(map[string]string) + for k, v := range d.Get("config").(map[string]interface{}) { + config[k] = v.(string) + } + + // Create the client to access our remote state + log.Printf("[DEBUG] Initializing remote state client: %s", backend) + client, err := remote.NewClient(backend, config) + if err != nil { + return err + } + + // Create the remote state itself and refresh it in order to load the state + log.Printf("[DEBUG] Loading remote state...") + state := &remote.State{Client: client} + if err := state.RefreshState(); err != nil { + return err + } + + d.SetId(time.Now().UTC().String()) + d.Set("output", state.State().RootModule().Outputs) + return nil +} + +func resourceStateDelete(d *schema.ResourceData, meta interface{}) error { + d.SetId("") + return nil +} diff --git a/builtin/providers/terraform/resource_state_test.go b/builtin/providers/terraform/resource_state_test.go new file mode 100644 index 000000000000..6db173503945 --- /dev/null +++ b/builtin/providers/terraform/resource_state_test.go @@ -0,0 +1,54 @@ +package terraform + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccState_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccState_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckStateValue( + "terraform_state.foo", "foo", "bar"), + ), + }, + }, + }) +} + +func testAccCheckStateValue(id, name, value string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[id] + if !ok { + return fmt.Errorf("Not found: %s", id) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + v := rs.Primary.Attributes["output."+name] + if v != value { + return fmt.Errorf( + "Value for %s is %s, not %s", name, v, value) + } + + return nil + } +} + +const testAccState_basic = ` +resource "terraform_state" "foo" { + backend = "_local" + + config { + path = "./test-fixtures/basic.tfstate" + } +}` From 164f303da40cd505edef22c75986e420b694331a Mon Sep 17 00:00:00 2001 From: Tarrant Date: Sun, 15 Mar 2015 16:12:25 -0700 Subject: [PATCH 03/59] Add SSH Agent support --- .../remote-exec/resource_provisioner.go | 7 +++-- helper/ssh/provisioner.go | 26 ++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/builtin/provisioners/remote-exec/resource_provisioner.go b/builtin/provisioners/remote-exec/resource_provisioner.go index b3f0d0c0e962..d190250a21d3 100644 --- a/builtin/provisioners/remote-exec/resource_provisioner.go +++ b/builtin/provisioners/remote-exec/resource_provisioner.go @@ -178,10 +178,13 @@ func (p *ResourceProvisioner) runScripts( " Host: %s\n"+ " User: %s\n"+ " Password: %v\n"+ - " Private key: %v", + " Private key: %v"+ + " SSH Agent: %v", conf.Host, conf.User, conf.Password != "", - conf.KeyFile != "")) + conf.KeyFile != "", + conf.Agent, + )) // Wait and retry until we establish the SSH connection var comm *helper.SSHCommunicator diff --git a/helper/ssh/provisioner.go b/helper/ssh/provisioner.go index 2d60d893477b..bf67d8a5be01 100644 --- a/helper/ssh/provisioner.go +++ b/helper/ssh/provisioner.go @@ -5,12 +5,15 @@ import ( "fmt" "io/ioutil" "log" + "net" + "os" "time" - "golang.org/x/crypto/ssh" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/go-homedir" "github.com/mitchellh/mapstructure" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" ) const ( @@ -37,6 +40,7 @@ type SSHConfig struct { KeyFile string `mapstructure:"key_file"` Host string Port int + Agent bool Timeout string ScriptPath string `mapstructure:"script_path"` TimeoutVal time.Duration `mapstructure:"-"` @@ -102,6 +106,26 @@ func PrepareConfig(conf *SSHConfig) (*Config, error) { sshConf := &ssh.ClientConfig{ User: conf.User, } + if conf.Agent { + sshAuthSock := os.Getenv("SSH_AUTH_SOCK") + + if sshAuthSock == "" { + return nil, fmt.Errorf("SSH Requested but SSH_AUTH_SOCK not-specified") + } + + conn, err := net.Dial("unix", sshAuthSock) + if err != nil { + return nil, fmt.Errorf("Error connecting to SSH_AUTH_SOCK: %v", err) + } + // I need to close this but, later after all connections have been made + // defer conn.Close() + signers, err := agent.NewClient(conn).Signers() + if err != nil { + return nil, fmt.Errorf("Error getting keys from ssh agent: %v", err) + } + + sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signers...)) + } if conf.KeyFile != "" { fullPath, err := homedir.Expand(conf.KeyFile) if err != nil { From 951f4201f7928111632ddec14a3db13fcb331aef Mon Sep 17 00:00:00 2001 From: Tarrant Date: Sun, 15 Mar 2015 16:37:33 -0700 Subject: [PATCH 04/59] Add documentation for ssh-agent --- website/source/docs/provisioners/connection.html.markdown | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/source/docs/provisioners/connection.html.markdown b/website/source/docs/provisioners/connection.html.markdown index af55fb2e433e..6d289c6dadb7 100644 --- a/website/source/docs/provisioners/connection.html.markdown +++ b/website/source/docs/provisioners/connection.html.markdown @@ -46,6 +46,8 @@ The following arguments are supported: * `key_file` - The SSH key to use for the connection. This takes preference over the password if provided. +* `agent` - Set to true to enable using ssh-agent to authenticate. + * `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. From 05407296c608ee15df8249fbd17c25de8b6fbe47 Mon Sep 17 00:00:00 2001 From: Tarrant Date: Fri, 20 Mar 2015 18:18:35 -0700 Subject: [PATCH 05/59] Add cleanup function to close SSHAgent --- .../provisioners/file/resource_provisioner.go | 1 + .../remote-exec/resource_provisioner.go | 1 + helper/ssh/communicator.go | 4 ++++ helper/ssh/provisioner.go | 18 +++++++++++++++--- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/builtin/provisioners/file/resource_provisioner.go b/builtin/provisioners/file/resource_provisioner.go index bb95c0860330..8b9e14570f1d 100644 --- a/builtin/provisioners/file/resource_provisioner.go +++ b/builtin/provisioners/file/resource_provisioner.go @@ -60,6 +60,7 @@ func (p *ResourceProvisioner) copyFiles(conf *helper.SSHConfig, src, dst string) if err != nil { return err } + defer config.CleanupConfig() // Wait and retry until we establish the SSH connection var comm *helper.SSHCommunicator diff --git a/builtin/provisioners/remote-exec/resource_provisioner.go b/builtin/provisioners/remote-exec/resource_provisioner.go index d190250a21d3..046e0e860cfb 100644 --- a/builtin/provisioners/remote-exec/resource_provisioner.go +++ b/builtin/provisioners/remote-exec/resource_provisioner.go @@ -172,6 +172,7 @@ func (p *ResourceProvisioner) runScripts( if err != nil { return err } + defer config.CleanupConfig() o.Output(fmt.Sprintf( "Connecting to remote host via SSH...\n"+ diff --git a/helper/ssh/communicator.go b/helper/ssh/communicator.go index 817f37368de9..f908de97dfa3 100644 --- a/helper/ssh/communicator.go +++ b/helper/ssh/communicator.go @@ -97,6 +97,10 @@ type Config struct { // NoPty, if true, will not request a pty from the remote end. NoPty bool + + // SSHAgentConn is a pointer to the UNIX connection for talking with the + // ssh-agent. + SSHAgentConn net.Conn } // New creates a new packer.Communicator implementation over SSH. This takes diff --git a/helper/ssh/provisioner.go b/helper/ssh/provisioner.go index bf67d8a5be01..bf8f526373d6 100644 --- a/helper/ssh/provisioner.go +++ b/helper/ssh/provisioner.go @@ -103,6 +103,9 @@ func safeDuration(dur string, defaultDur time.Duration) time.Duration { // PrepareConfig is used to turn the *SSHConfig provided into a // usable *Config for client initialization. func PrepareConfig(conf *SSHConfig) (*Config, error) { + var conn net.Conn + var err error + sshConf := &ssh.ClientConfig{ User: conf.User, } @@ -113,7 +116,7 @@ func PrepareConfig(conf *SSHConfig) (*Config, error) { return nil, fmt.Errorf("SSH Requested but SSH_AUTH_SOCK not-specified") } - conn, err := net.Dial("unix", sshAuthSock) + conn, err = net.Dial("unix", sshAuthSock) if err != nil { return nil, fmt.Errorf("Error connecting to SSH_AUTH_SOCK: %v", err) } @@ -164,8 +167,17 @@ func PrepareConfig(conf *SSHConfig) (*Config, error) { } host := fmt.Sprintf("%s:%d", conf.Host, conf.Port) config := &Config{ - SSHConfig: sshConf, - Connection: ConnectFunc("tcp", host), + SSHConfig: sshConf, + Connection: ConnectFunc("tcp", host), + SSHAgentConn: conn, } return config, nil } + +func (c *Config) CleanupConfig() error { + if c.SSHAgentConn != nil { + return c.SSHAgentConn.Close() + } + + return nil +} From 87907e24bae952d9992253f49573e76628d837e9 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Tue, 24 Mar 2015 15:34:13 -0500 Subject: [PATCH 06/59] provider/aws: Introduce IAM connection --- builtin/providers/aws/config.go | 4 + .../providers/aws/resource_aws_db_instance.go | 55 +++++++++++ builtin/providers/aws/tagsRDS.go | 94 +++++++++++++++++++ builtin/providers/aws/tagsRDS_test.go | 85 +++++++++++++++++ 4 files changed, 238 insertions(+) create mode 100644 builtin/providers/aws/tagsRDS.go create mode 100644 builtin/providers/aws/tagsRDS_test.go diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 672af6f0794c..7e84d3a279bb 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/aws-sdk-go/gen/autoscaling" "github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/aws-sdk-go/gen/elb" + "github.com/hashicorp/aws-sdk-go/gen/iam" "github.com/hashicorp/aws-sdk-go/gen/rds" "github.com/hashicorp/aws-sdk-go/gen/route53" "github.com/hashicorp/aws-sdk-go/gen/s3" @@ -30,6 +31,7 @@ type AWSClient struct { r53conn *route53.Route53 region string rdsconn *rds.RDS + iamconn *iam.IAM } // Client configures and returns a fully initailized AWSClient @@ -70,6 +72,8 @@ func (c *Config) Client() (interface{}, error) { client.r53conn = route53.New(creds, "us-east-1", nil) log.Println("[INFO] Initializing EC2 Connection") client.ec2conn = ec2.New(creds, c.Region, nil) + + client.iamconn = iam.New(creds, c.Region, nil) } if len(errs) > 0 { diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go index e99744a0f31b..c445a630b078 100644 --- a/builtin/providers/aws/resource_aws_db_instance.go +++ b/builtin/providers/aws/resource_aws_db_instance.go @@ -6,6 +6,7 @@ import ( "time" "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/iam" "github.com/hashicorp/aws-sdk-go/gen/rds" "github.com/hashicorp/terraform/helper/hashcode" @@ -17,6 +18,7 @@ func resourceAwsDbInstance() *schema.Resource { return &schema.Resource{ Create: resourceAwsDbInstanceCreate, Read: resourceAwsDbInstanceRead, + Update: resourceAwsDbInstanceUpdate, Delete: resourceAwsDbInstanceDelete, Schema: map[string]*schema.Schema{ @@ -185,12 +187,14 @@ func resourceAwsDbInstance() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "tags": tagsSchema(), }, } } func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).rdsconn + tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{})) opts := rds.CreateDBInstanceMessage{ AllocatedStorage: aws.Integer(d.Get("allocated_storage").(int)), DBInstanceClass: aws.String(d.Get("instance_class").(string)), @@ -201,6 +205,7 @@ func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error Engine: aws.String(d.Get("engine").(string)), EngineVersion: aws.String(d.Get("engine_version").(string)), StorageEncrypted: aws.Boolean(d.Get("storage_encrypted").(bool)), + Tags: tags, } if attr, ok := d.GetOk("storage_type"); ok { @@ -328,6 +333,28 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error { d.Set("status", *v.DBInstanceStatus) d.Set("storage_encrypted", *v.StorageEncrypted) + // list tags for resource + // set tags + conn := meta.(*AWSClient).rdsconn + arn, err := buildRDSARN(d, meta) + if err != nil { + log.Printf("[DEBUG] Error building ARN for DB Instance, not setting Tags for DB %s", *v.DBName) + } else { + resp, err := conn.ListTagsForResource(&rds.ListTagsForResourceMessage{ + ResourceName: aws.String(arn), + }) + + if err != nil { + log.Print("[DEBUG] Error retreiving tags for ARN: %s", arn) + } + + var dt []rds.Tag + if len(resp.TagList) > 0 { + dt = resp.TagList + } + d.Set("tags", tagsToMapRDS(dt)) + } + // Create an empty schema.Set to hold all vpc security group ids ids := &schema.Set{ F: func(v interface{}) int { @@ -390,6 +417,21 @@ func resourceAwsDbInstanceDelete(d *schema.ResourceData, meta interface{}) error return nil } +func resourceAwsDbInstanceUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).rdsconn + + d.Partial(true) + if arn, err := buildRDSARN(d, meta); err != nil { + if err := setTagsRDS(conn, d, arn); err != nil { + return err + } else { + d.SetPartial("tags") + } + } + d.Partial(false) + return resourceAwsDbInstanceRead(d, meta) +} + func resourceAwsBbInstanceRetrieve( d *schema.ResourceData, meta interface{}) (*rds.DBInstance, error) { conn := meta.(*AWSClient).rdsconn @@ -439,3 +481,16 @@ func resourceAwsDbInstanceStateRefreshFunc( return v, *v.DBInstanceStatus, nil } } + +func buildRDSARN(d *schema.ResourceData, meta interface{}) (string, error) { + iamconn := meta.(*AWSClient).iamconn + region := meta.(*AWSClient).region + // An zero value GetUserRequest{} defers to the currently logged in user + resp, err := iamconn.GetUser(&iam.GetUserRequest{}) + if err != nil { + return "", err + } + user := resp.User + arn := fmt.Sprintf("arn:aws:rds:%s:%s:db:%s", region, *user.UserID, d.Id()) + return arn, nil +} diff --git a/builtin/providers/aws/tagsRDS.go b/builtin/providers/aws/tagsRDS.go new file mode 100644 index 000000000000..0677c23209d6 --- /dev/null +++ b/builtin/providers/aws/tagsRDS.go @@ -0,0 +1,94 @@ +package aws + +import ( + "log" + + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/rds" + "github.com/hashicorp/terraform/helper/schema" +) + +// setTags is a helper to set the tags for a resource. It expects the +// tags field to be named "tags" +func setTagsRDS(conn *rds.RDS, d *schema.ResourceData, arn string) error { + if d.HasChange("tags") { + oraw, nraw := d.GetChange("tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + create, remove := diffTagsRDS(tagsFromMapRDS(o), tagsFromMapRDS(n)) + + // Set tags + if len(remove) > 0 { + log.Printf("[DEBUG] Removing tags: %#v", remove) + k := make([]string, 0, len(remove)) + for i, t := range remove { + k[i] = *t.Key + } + err := conn.RemoveTagsFromResource(&rds.RemoveTagsFromResourceMessage{ + ResourceName: aws.String(d.Get("name").(string)), + TagKeys: k, + }) + if err != nil { + return err + } + } + if len(create) > 0 { + log.Printf("[DEBUG] Creating tags: %#v", create) + err := conn.AddTagsToResource(&rds.AddTagsToResourceMessage{ + ResourceName: aws.String(arn), + Tags: create, + }) + if err != nil { + return err + } + } + } + + return nil +} + +// diffTags takes our tags locally and the ones remotely and returns +// the set of tags that must be created, and the set of tags that must +// be destroyed. +func diffTagsRDS(oldTags, newTags []rds.Tag) ([]rds.Tag, []rds.Tag) { + // First, we're creating everything we have + create := make(map[string]interface{}) + for _, t := range newTags { + create[*t.Key] = *t.Value + } + + // Build the list of what to remove + var remove []rds.Tag + for _, t := range oldTags { + old, ok := create[*t.Key] + if !ok || old != *t.Value { + // Delete it! + remove = append(remove, t) + } + } + + return tagsFromMapRDS(create), remove +} + +// tagsFromMap returns the tags for the given map of data. +func tagsFromMapRDS(m map[string]interface{}) []rds.Tag { + result := make([]rds.Tag, 0, len(m)) + for k, v := range m { + result = append(result, rds.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + }) + } + + return result +} + +// tagsToMap turns the list of tags into a map. +func tagsToMapRDS(ts []rds.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + result[*t.Key] = *t.Value + } + + return result +} diff --git a/builtin/providers/aws/tagsRDS_test.go b/builtin/providers/aws/tagsRDS_test.go new file mode 100644 index 000000000000..1d9da835751e --- /dev/null +++ b/builtin/providers/aws/tagsRDS_test.go @@ -0,0 +1,85 @@ +package aws + +import ( + "fmt" + "reflect" + "testing" + + "github.com/hashicorp/aws-sdk-go/gen/rds" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestDiffRDSTags(t *testing.T) { + cases := []struct { + Old, New map[string]interface{} + Create, Remove map[string]string + }{ + // Basic add/remove + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "bar": "baz", + }, + Create: map[string]string{ + "bar": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + + // Modify + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "foo": "baz", + }, + Create: map[string]string{ + "foo": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + } + + for i, tc := range cases { + c, r := diffTagsRDS(tagsFromMapRDS(tc.Old), tagsFromMapRDS(tc.New)) + cm := tagsToMapRDS(c) + rm := tagsToMapRDS(r) + if !reflect.DeepEqual(cm, tc.Create) { + t.Fatalf("%d: bad create: %#v", i, cm) + } + if !reflect.DeepEqual(rm, tc.Remove) { + t.Fatalf("%d: bad remove: %#v", i, rm) + } + } +} + +// testAccCheckTags can be used to check the tags on a resource. +func testAccCheckRDSTags( + ts *[]rds.Tag, key string, value string) resource.TestCheckFunc { + return func(s *terraform.State) error { + m := tagsToMapRDS(*ts) + v, ok := m[key] + if value != "" && !ok { + return fmt.Errorf("Missing tag: %s", key) + } else if value == "" && ok { + return fmt.Errorf("Extra tag: %s", key) + } + if value == "" { + return nil + } + + if v != value { + return fmt.Errorf("%s: bad value: %s", key, v) + } + + return nil + } +} From 676f3c5bab72b1e7b4ac40d61cc9d2047d5405d8 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Wed, 25 Mar 2015 10:05:15 -0500 Subject: [PATCH 07/59] fix typo --- builtin/providers/aws/resource_aws_db_instance.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go index c445a630b078..6e6e84f5b386 100644 --- a/builtin/providers/aws/resource_aws_db_instance.go +++ b/builtin/providers/aws/resource_aws_db_instance.go @@ -421,7 +421,7 @@ func resourceAwsDbInstanceUpdate(d *schema.ResourceData, meta interface{}) error conn := meta.(*AWSClient).rdsconn d.Partial(true) - if arn, err := buildRDSARN(d, meta); err != nil { + if arn, err := buildRDSARN(d, meta); err == nil { if err := setTagsRDS(conn, d, arn); err != nil { return err } else { From 65ff5b327d71f1445a1e2a137667df2af6666e4b Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Wed, 25 Mar 2015 10:14:45 -0500 Subject: [PATCH 08/59] upgrade VPC Ids and DB Subnet to be optionally computed --- builtin/providers/aws/resource_aws_db_instance.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go index 6e6e84f5b386..211400169351 100644 --- a/builtin/providers/aws/resource_aws_db_instance.go +++ b/builtin/providers/aws/resource_aws_db_instance.go @@ -140,6 +140,7 @@ func resourceAwsDbInstance() *schema.Resource { "vpc_security_group_ids": &schema.Schema{ Type: schema.TypeSet, Optional: true, + Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: func(v interface{}) int { return hashcode.String(v.(string)) @@ -164,6 +165,7 @@ func resourceAwsDbInstance() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, + Computed: true, }, "parameter_group_name": &schema.Schema{ From 398f4564c4a3b6877a3134f29d4dbf4220168d77 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Wed, 25 Mar 2015 10:32:54 -0500 Subject: [PATCH 09/59] fix formatting --- builtin/providers/aws/resource_aws_db_instance.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go index 211400169351..b690c9a1afdc 100644 --- a/builtin/providers/aws/resource_aws_db_instance.go +++ b/builtin/providers/aws/resource_aws_db_instance.go @@ -347,7 +347,7 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error { }) if err != nil { - log.Print("[DEBUG] Error retreiving tags for ARN: %s", arn) + log.Printf("[DEBUG] Error retreiving tags for ARN: %s", arn) } var dt []rds.Tag From 89854b0af5fcfdea512a4017ceab4c9420da8ec3 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Wed, 25 Mar 2015 11:10:12 -0500 Subject: [PATCH 10/59] fix index out of range error --- builtin/providers/aws/tagsRDS.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/builtin/providers/aws/tagsRDS.go b/builtin/providers/aws/tagsRDS.go index 0677c23209d6..8eb592427854 100644 --- a/builtin/providers/aws/tagsRDS.go +++ b/builtin/providers/aws/tagsRDS.go @@ -20,12 +20,13 @@ func setTagsRDS(conn *rds.RDS, d *schema.ResourceData, arn string) error { // Set tags if len(remove) > 0 { log.Printf("[DEBUG] Removing tags: %#v", remove) - k := make([]string, 0, len(remove)) + k := make([]string, len(remove), len(remove)) for i, t := range remove { k[i] = *t.Key } + err := conn.RemoveTagsFromResource(&rds.RemoveTagsFromResourceMessage{ - ResourceName: aws.String(d.Get("name").(string)), + ResourceName: aws.String(arn), TagKeys: k, }) if err != nil { From b64a919d83b44fcba5a6e754401e0429115e1153 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Thu, 26 Mar 2015 16:45:23 -0500 Subject: [PATCH 11/59] provider/aws: Add tags to Route53 hosted zones --- .../aws/resource_aws_route53_zone.go | 34 +++++++- builtin/providers/aws/tags_route53.go | 87 +++++++++++++++++++ builtin/providers/aws/tags_route53_test.go | 85 ++++++++++++++++++ 3 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 builtin/providers/aws/tags_route53.go create mode 100644 builtin/providers/aws/tags_route53_test.go diff --git a/builtin/providers/aws/resource_aws_route53_zone.go b/builtin/providers/aws/resource_aws_route53_zone.go index 6d9914b7f088..e6c8be5715ee 100644 --- a/builtin/providers/aws/resource_aws_route53_zone.go +++ b/builtin/providers/aws/resource_aws_route53_zone.go @@ -16,6 +16,7 @@ func resourceAwsRoute53Zone() *schema.Resource { return &schema.Resource{ Create: resourceAwsRoute53ZoneCreate, Read: resourceAwsRoute53ZoneRead, + Update: resourceAwsRoute53ZoneUpdate, Delete: resourceAwsRoute53ZoneDelete, Schema: map[string]*schema.Schema{ @@ -29,6 +30,8 @@ func resourceAwsRoute53Zone() *schema.Resource { Type: schema.TypeString, Computed: true, }, + + "tags": tagsSchema(), }, } } @@ -72,7 +75,7 @@ func resourceAwsRoute53ZoneCreate(d *schema.ResourceData, meta interface{}) erro if err != nil { return err } - return nil + return resourceAwsRoute53ZoneUpdate(d, meta) } func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error { @@ -87,9 +90,38 @@ func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error return err } + // get tags + req := &route53.ListTagsForResourceRequest{ + ResourceID: aws.String(d.Id()), + ResourceType: aws.String("hostedzone"), + } + + resp, err := r53.ListTagsForResource(req) + if err != nil { + return err + } + + var tags []route53.Tag + if resp.ResourceTagSet != nil { + tags = resp.ResourceTagSet.Tags + } + d.Set("tags", tagsToMapR53(tags)) + return nil } +func resourceAwsRoute53ZoneUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).r53conn + + if err := setTagsR53(conn, d); err != nil { + return err + } else { + d.SetPartial("tags") + } + + return resourceAwsRoute53ZoneRead(d, meta) +} + func resourceAwsRoute53ZoneDelete(d *schema.ResourceData, meta interface{}) error { r53 := meta.(*AWSClient).r53conn diff --git a/builtin/providers/aws/tags_route53.go b/builtin/providers/aws/tags_route53.go new file mode 100644 index 000000000000..374c4bc32600 --- /dev/null +++ b/builtin/providers/aws/tags_route53.go @@ -0,0 +1,87 @@ +package aws + +import ( + "log" + + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/route53" + "github.com/hashicorp/terraform/helper/schema" +) + +// setTags is a helper to set the tags for a resource. It expects the +// tags field to be named "tags" +func setTagsR53(conn *route53.Route53, d *schema.ResourceData) error { + if d.HasChange("tags") { + oraw, nraw := d.GetChange("tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + create, remove := diffTagsR53(tagsFromMapR53(o), tagsFromMapR53(n)) + + // Set tags + r := make([]string, len(remove)) + for i, t := range remove { + r[i] = *t.Key + } + log.Printf("[DEBUG] Changing tags: \n\tadding: %#v\n\tremoving:", create, remove) + req := &route53.ChangeTagsForResourceRequest{ + AddTags: create, + RemoveTagKeys: r, + ResourceID: aws.String(d.Id()), + ResourceType: aws.String("hostedzone"), + } + resp, err := conn.ChangeTagsForResource(req) + log.Printf("\n\t****\nresp:\n%#v", resp) + if err != nil { + log.Printf("\n\t****\nerror:\n%#v", err) + return err + } + } + + return nil +} + +// diffTags takes our tags locally and the ones remotely and returns +// the set of tags that must be created, and the set of tags that must +// be destroyed. +func diffTagsR53(oldTags, newTags []route53.Tag) ([]route53.Tag, []route53.Tag) { + // First, we're creating everything we have + create := make(map[string]interface{}) + for _, t := range newTags { + create[*t.Key] = *t.Value + } + + // Build the list of what to remove + var remove []route53.Tag + for _, t := range oldTags { + old, ok := create[*t.Key] + if !ok || old != *t.Value { + // Delete it! + remove = append(remove, t) + } + } + + return tagsFromMapR53(create), remove +} + +// tagsFromMap returns the tags for the given map of data. +func tagsFromMapR53(m map[string]interface{}) []route53.Tag { + result := make([]route53.Tag, 0, len(m)) + for k, v := range m { + result = append(result, route53.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + }) + } + + return result +} + +// tagsToMap turns the list of tags into a map. +func tagsToMapR53(ts []route53.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + result[*t.Key] = *t.Value + } + + return result +} diff --git a/builtin/providers/aws/tags_route53_test.go b/builtin/providers/aws/tags_route53_test.go new file mode 100644 index 000000000000..79ce1e513ecf --- /dev/null +++ b/builtin/providers/aws/tags_route53_test.go @@ -0,0 +1,85 @@ +package aws + +import ( + "fmt" + "reflect" + "testing" + + "github.com/hashicorp/aws-sdk-go/gen/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestDiffTagsR53(t *testing.T) { + cases := []struct { + Old, New map[string]interface{} + Create, Remove map[string]string + }{ + // Basic add/remove + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "bar": "baz", + }, + Create: map[string]string{ + "bar": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + + // Modify + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "foo": "baz", + }, + Create: map[string]string{ + "foo": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + } + + for i, tc := range cases { + c, r := diffTags(tagsFromMap(tc.Old), tagsFromMap(tc.New)) + cm := tagsToMap(c) + rm := tagsToMap(r) + if !reflect.DeepEqual(cm, tc.Create) { + t.Fatalf("%d: bad create: %#v", i, cm) + } + if !reflect.DeepEqual(rm, tc.Remove) { + t.Fatalf("%d: bad remove: %#v", i, rm) + } + } +} + +// testAccCheckTags can be used to check the tags on a resource. +func testAccCheckTagsR53( + ts *[]ec2.Tag, key string, value string) resource.TestCheckFunc { + return func(s *terraform.State) error { + m := tagsToMap(*ts) + v, ok := m[key] + if value != "" && !ok { + return fmt.Errorf("Missing tag: %s", key) + } else if value == "" && ok { + return fmt.Errorf("Extra tag: %s", key) + } + if value == "" { + return nil + } + + if v != value { + return fmt.Errorf("%s: bad value: %s", key, v) + } + + return nil + } +} From 12585b19638bd5eb06a5edf88bb3f0ad7e16c39e Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Fri, 27 Mar 2015 15:41:42 -0500 Subject: [PATCH 12/59] provider/aws: Finish Tag support for Route 53 zone --- .../aws/resource_aws_route53_zone_test.go | 43 +++++++++++++++++-- builtin/providers/aws/tags_route53.go | 4 +- builtin/providers/aws/tags_route53_test.go | 12 +++--- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/builtin/providers/aws/resource_aws_route53_zone_test.go b/builtin/providers/aws/resource_aws_route53_zone_test.go index fa78634cf79b..d6e4af2f22c6 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_test.go @@ -63,6 +63,9 @@ func TestCleanChangeID(t *testing.T) { } func TestAccRoute53Zone(t *testing.T) { + var zone route53.HostedZone + var td route53.ResourceTagSet + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -71,7 +74,9 @@ func TestAccRoute53Zone(t *testing.T) { resource.TestStep{ Config: testAccRoute53ZoneConfig, Check: resource.ComposeTestCheckFunc( - testAccCheckRoute53ZoneExists("aws_route53_zone.main"), + testAccCheckRoute53ZoneExists("aws_route53_zone.main", &zone), + testAccLoadTagsR53(&zone, &td), + testAccCheckTagsR53(&td.Tags, "foo", "bar"), ), }, }, @@ -93,7 +98,7 @@ func testAccCheckRoute53ZoneDestroy(s *terraform.State) error { return nil } -func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc { +func testAccCheckRoute53ZoneExists(n string, zone *route53.HostedZone) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -105,10 +110,37 @@ func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc { } conn := testAccProvider.Meta().(*AWSClient).r53conn - _, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(rs.Primary.ID)}) + resp, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(rs.Primary.ID)}) if err != nil { return fmt.Errorf("Hosted zone err: %v", err) } + *zone = *resp.HostedZone + return nil + } +} + +func testAccLoadTagsR53(zone *route53.HostedZone, td *route53.ResourceTagSet) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).r53conn + + zone := cleanZoneID(*zone.ID) + req := &route53.ListTagsForResourceRequest{ + ResourceID: aws.String(zone), + ResourceType: aws.String("hostedzone"), + } + + resp, err := conn.ListTagsForResource(req) + if err != nil { + return err + } + + var tags []route53.Tag + if resp.ResourceTagSet != nil { + tags = resp.ResourceTagSet.Tags + } + + *td = *resp.ResourceTagSet + return nil } } @@ -116,5 +148,10 @@ func testAccCheckRoute53ZoneExists(n string) resource.TestCheckFunc { const testAccRoute53ZoneConfig = ` resource "aws_route53_zone" "main" { name = "hashicorp.com" + + tags { + foo = "bar" + Name = "tf-route53-tag-test" + } } ` diff --git a/builtin/providers/aws/tags_route53.go b/builtin/providers/aws/tags_route53.go index 374c4bc32600..8e31fc3739db 100644 --- a/builtin/providers/aws/tags_route53.go +++ b/builtin/providers/aws/tags_route53.go @@ -22,7 +22,7 @@ func setTagsR53(conn *route53.Route53, d *schema.ResourceData) error { for i, t := range remove { r[i] = *t.Key } - log.Printf("[DEBUG] Changing tags: \n\tadding: %#v\n\tremoving:", create, remove) + log.Printf("[DEBUG] Changing tags: \n\tadding: %#v\n\tremoving:%#v", create, remove) req := &route53.ChangeTagsForResourceRequest{ AddTags: create, RemoveTagKeys: r, @@ -30,9 +30,7 @@ func setTagsR53(conn *route53.Route53, d *schema.ResourceData) error { ResourceType: aws.String("hostedzone"), } resp, err := conn.ChangeTagsForResource(req) - log.Printf("\n\t****\nresp:\n%#v", resp) if err != nil { - log.Printf("\n\t****\nerror:\n%#v", err) return err } } diff --git a/builtin/providers/aws/tags_route53_test.go b/builtin/providers/aws/tags_route53_test.go index 79ce1e513ecf..40a4154f3f46 100644 --- a/builtin/providers/aws/tags_route53_test.go +++ b/builtin/providers/aws/tags_route53_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/hashicorp/aws-sdk-go/gen/ec2" + "github.com/hashicorp/aws-sdk-go/gen/route53" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) @@ -49,9 +49,9 @@ func TestDiffTagsR53(t *testing.T) { } for i, tc := range cases { - c, r := diffTags(tagsFromMap(tc.Old), tagsFromMap(tc.New)) - cm := tagsToMap(c) - rm := tagsToMap(r) + c, r := diffTagsR53(tagsFromMapR53(tc.Old), tagsFromMapR53(tc.New)) + cm := tagsToMapR53(c) + rm := tagsToMapR53(r) if !reflect.DeepEqual(cm, tc.Create) { t.Fatalf("%d: bad create: %#v", i, cm) } @@ -63,9 +63,9 @@ func TestDiffTagsR53(t *testing.T) { // testAccCheckTags can be used to check the tags on a resource. func testAccCheckTagsR53( - ts *[]ec2.Tag, key string, value string) resource.TestCheckFunc { + ts *[]route53.Tag, key string, value string) resource.TestCheckFunc { return func(s *terraform.State) error { - m := tagsToMap(*ts) + m := tagsToMapR53(*ts) v, ok := m[key] if value != "" && !ok { return fmt.Errorf("Missing tag: %s", key) From ce8ec26d0803ce31c40c9403a1acc0a2add8847c Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Fri, 27 Mar 2015 16:05:54 -0500 Subject: [PATCH 13/59] cleanups --- builtin/providers/aws/resource_aws_route53_zone_test.go | 5 +---- builtin/providers/aws/tags_route53.go | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/builtin/providers/aws/resource_aws_route53_zone_test.go b/builtin/providers/aws/resource_aws_route53_zone_test.go index d6e4af2f22c6..0669f88b1cde 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_test.go @@ -134,13 +134,10 @@ func testAccLoadTagsR53(zone *route53.HostedZone, td *route53.ResourceTagSet) re return err } - var tags []route53.Tag if resp.ResourceTagSet != nil { - tags = resp.ResourceTagSet.Tags + *td = *resp.ResourceTagSet } - *td = *resp.ResourceTagSet - return nil } } diff --git a/builtin/providers/aws/tags_route53.go b/builtin/providers/aws/tags_route53.go index 8e31fc3739db..e5251d02a077 100644 --- a/builtin/providers/aws/tags_route53.go +++ b/builtin/providers/aws/tags_route53.go @@ -29,7 +29,8 @@ func setTagsR53(conn *route53.Route53, d *schema.ResourceData) error { ResourceID: aws.String(d.Id()), ResourceType: aws.String("hostedzone"), } - resp, err := conn.ChangeTagsForResource(req) + + _, err := conn.ChangeTagsForResource(req) if err != nil { return err } From e485767694574174b316ad6834ade7489463a15d Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Tue, 31 Mar 2015 09:41:37 -0500 Subject: [PATCH 14/59] provider/aws: Add non-destructive updates to AWS RDS This introduces non-destructive, in-place upgrades to MultiAZ and Engine Version attributes of AWS RDS instances. --- .../providers/aws/resource_aws_db_instance.go | 41 ++++++++++++++++++- .../providers/aws/r/db_instance.html.markdown | 3 ++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go index b690c9a1afdc..14deff73229b 100644 --- a/builtin/providers/aws/resource_aws_db_instance.go +++ b/builtin/providers/aws/resource_aws_db_instance.go @@ -49,7 +49,6 @@ func resourceAwsDbInstance() *schema.Resource { "engine_version": &schema.Schema{ Type: schema.TypeString, Required: true, - ForceNew: true, }, "storage_encrypted": &schema.Schema{ @@ -121,7 +120,6 @@ func resourceAwsDbInstance() *schema.Resource { Type: schema.TypeBool, Optional: true, Computed: true, - ForceNew: true, }, "port": &schema.Schema{ @@ -189,6 +187,16 @@ func resourceAwsDbInstance() *schema.Resource { Type: schema.TypeString, Computed: true, }, + + // apply_immediately is used to determine when the update modifications + // take place. + // See http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html + "apply_immediately": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "tags": tagsSchema(), }, } @@ -423,6 +431,35 @@ func resourceAwsDbInstanceUpdate(d *schema.ResourceData, meta interface{}) error conn := meta.(*AWSClient).rdsconn d.Partial(true) + // Change is used to determine if a ModifyDBInstanceMessage request actually + // gets sent. + change := false + + req := &rds.ModifyDBInstanceMessage{ + ApplyImmediately: aws.Boolean(d.Get("apply_immediately").(bool)), + DBInstanceIdentifier: aws.String(d.Id()), + } + + if d.HasChange("engine_version") { + change = true + d.SetPartial("engine_version") + req.EngineVersion = aws.String(d.Get("engine_version").(string)) + } + + if d.HasChange("multi_az") { + change = true + d.SetPartial("multi_az") + req.MultiAZ = aws.Boolean(d.Get("multi_az").(bool)) + } + + if change { + log.Printf("[DEBUG] DB Instance Modification request: %#v", req) + _, err := conn.ModifyDBInstance(req) + if err != nil { + return fmt.Errorf("Error mofigying DB Instance %s: %s", d.Id(), err) + } + } + if arn, err := buildRDSARN(d, meta); err == nil { if err := setTagsRDS(conn, d, arn); err != nil { return err diff --git a/website/source/docs/providers/aws/r/db_instance.html.markdown b/website/source/docs/providers/aws/r/db_instance.html.markdown index 4b22531157a0..6b3b97552ff2 100644 --- a/website/source/docs/providers/aws/r/db_instance.html.markdown +++ b/website/source/docs/providers/aws/r/db_instance.html.markdown @@ -62,6 +62,9 @@ The following arguments are supported: * `db_subnet_group_name` - (Optional) Name of DB subnet group * `parameter_group_name` - (Optional) Name of the DB parameter group to associate. * `storage_encrypted` - (Optional) Specifies whether the DB instance is encrypted. The Default is `false` if not specified. +* `apply_immediately` - (Optional) Specifies whether any database modifications + are applied immediately, or during the next maintenance window. Default is + `False`. See [Amazon RDS Documentation for more for more information.](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html) ## Attributes Reference From 97acccd3eda70f7552cecd71ea8f5916b215b694 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Tue, 24 Mar 2015 11:18:15 -0500 Subject: [PATCH 15/59] core: targeted operations Add `-target=resource` flag to core operations, allowing users to target specific resources in their infrastructure. When `-target` is used, the operation will only apply to that resource and its dependencies. The calculated dependencies are different depending on whether we're running a normal operation or a `terraform destroy`. Generally, "dependencies" refers to ancestors: resources falling _before_ the target in the graph, because their changes are required to accurately act on the target. For destroys, "dependencies" are descendents: those resources which fall _after_ the target. These resources depend on our target, which is going to be destroyed, so they should also be destroyed. --- command/apply.go | 16 +- command/apply_destroy_test.go | 90 ++++++ command/flag_kv.go | 14 + command/meta.go | 11 + command/plan.go | 7 +- command/refresh.go | 4 + .../apply-destroy-targeted/main.tf | 7 + dag/dag.go | 93 +++++- dag/dag_test.go | 62 ++++ helper/resource/testing.go | 3 +- terraform/context.go | 12 +- terraform/context_test.go | 266 ++++++++++-------- terraform/graph_builder.go | 11 + terraform/plan.go | 9 - .../transform-targets-basic/main.tf | 16 ++ .../transform-targets-destroy/main.tf | 18 ++ terraform/transform_targets.go | 77 +++++ terraform/transform_targets_test.go | 71 +++++ 18 files changed, 637 insertions(+), 150 deletions(-) create mode 100644 command/test-fixtures/apply-destroy-targeted/main.tf create mode 100644 terraform/test-fixtures/transform-targets-basic/main.tf create mode 100644 terraform/test-fixtures/transform-targets-destroy/main.tf create mode 100644 terraform/transform_targets.go create mode 100644 terraform/transform_targets_test.go diff --git a/command/apply.go b/command/apply.go index d46b71679814..529d6e701a87 100644 --- a/command/apply.go +++ b/command/apply.go @@ -93,6 +93,7 @@ func (c *ApplyCommand) Run(args []string) int { // Build the context based on the arguments given ctx, planned, err := c.Context(contextOpts{ + Destroy: c.Destroy, Path: configPath, StatePath: c.Meta.statePath, }) @@ -140,12 +141,7 @@ func (c *ApplyCommand) Run(args []string) int { } } - var opts terraform.PlanOpts - if c.Destroy { - opts.Destroy = true - } - - if _, err := ctx.Plan(&opts); err != nil { + if _, err := ctx.Plan(); err != nil { c.Ui.Error(fmt.Sprintf( "Error creating plan: %s", err)) return 1 @@ -319,6 +315,10 @@ Options: "-state". This can be used to preserve the old state. + -target=resource Resource to target. Operation will be limited to this + resource and its dependencies. This flag can be used + multiple times. + -var 'foo=bar' Set a variable in the Terraform configuration. This flag can be set multiple times. @@ -357,6 +357,10 @@ Options: "-state". This can be used to preserve the old state. + -target=resource Resource to target. Operation will be limited to this + resource and its dependencies. This flag can be used + multiple times. + -var 'foo=bar' Set a variable in the Terraform configuration. This flag can be set multiple times. diff --git a/command/apply_destroy_test.go b/command/apply_destroy_test.go index bdc2440f0bce..63afb15edb32 100644 --- a/command/apply_destroy_test.go +++ b/command/apply_destroy_test.go @@ -116,6 +116,96 @@ func TestApply_destroyPlan(t *testing.T) { } } +func TestApply_destroyTargeted(t *testing.T) { + originalState := &terraform.State{ + Modules: []*terraform.ModuleState{ + &terraform.ModuleState{ + Path: []string{"root"}, + Resources: map[string]*terraform.ResourceState{ + "test_instance.foo": &terraform.ResourceState{ + Type: "test_instance", + Primary: &terraform.InstanceState{ + ID: "i-ab123", + }, + }, + "test_load_balancer.foo": &terraform.ResourceState{ + Type: "test_load_balancer", + Primary: &terraform.InstanceState{ + ID: "lb-abc123", + }, + }, + }, + }, + }, + } + + statePath := testStateFile(t, originalState) + + p := testProvider() + ui := new(cli.MockUi) + c := &ApplyCommand{ + Destroy: true, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + // Run the apply command pointing to our existing state + args := []string{ + "-force", + "-target", "test_instance.foo", + "-state", statePath, + testFixturePath("apply-destroy-targeted"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Verify a new state exists + if _, err := os.Stat(statePath); err != nil { + t.Fatalf("err: %s", err) + } + + f, err := os.Open(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + defer f.Close() + + state, err := terraform.ReadState(f) + if err != nil { + t.Fatalf("err: %s", err) + } + if state == nil { + t.Fatal("state should not be nil") + } + + actualStr := strings.TrimSpace(state.String()) + expectedStr := strings.TrimSpace(testApplyDestroyStr) + if actualStr != expectedStr { + t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) + } + + // Should have a backup file + f, err = os.Open(statePath + DefaultBackupExtention) + if err != nil { + t.Fatalf("err: %s", err) + } + + backupState, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + actualStr = strings.TrimSpace(backupState.String()) + expectedStr = strings.TrimSpace(originalState.String()) + if actualStr != expectedStr { + t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", actualStr, expectedStr) + } +} + const testApplyDestroyStr = ` ` diff --git a/command/flag_kv.go b/command/flag_kv.go index fd9b57b3a4a1..6e019877849e 100644 --- a/command/flag_kv.go +++ b/command/flag_kv.go @@ -85,3 +85,17 @@ func loadKVFile(rawPath string) (map[string]string, error) { return result, nil } + +// FlagStringSlice is a flag.Value implementation for parsing targets from the +// command line, e.g. -target=aws_instance.foo -target=aws_vpc.bar + +type FlagStringSlice []string + +func (v *FlagStringSlice) String() string { + return "" +} +func (v *FlagStringSlice) Set(raw string) error { + *v = append(*v, raw) + + return nil +} diff --git a/command/meta.go b/command/meta.go index 28a01c542aba..b542304af929 100644 --- a/command/meta.go +++ b/command/meta.go @@ -38,6 +38,9 @@ type Meta struct { input bool variables map[string]string + // Targets for this context (private) + targets []string + color bool oldUi cli.Ui @@ -126,6 +129,9 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) { m.statePath = copts.StatePath } + // Tell the context if we're in a destroy plan / apply + opts.Destroy = copts.Destroy + // Store the loaded state state, err := m.State() if err != nil { @@ -267,6 +273,7 @@ func (m *Meta) contextOpts() *terraform.ContextOpts { vs[k] = v } opts.Variables = vs + opts.Targets = m.targets opts.UIInput = m.UIInput() return &opts @@ -278,6 +285,7 @@ func (m *Meta) flagSet(n string) *flag.FlagSet { f.BoolVar(&m.input, "input", true, "input") f.Var((*FlagKV)(&m.variables), "var", "variables") f.Var((*FlagKVFile)(&m.variables), "var-file", "variable file") + f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target") if m.autoKey != "" { f.Var((*FlagKVFile)(&m.autoVariables), m.autoKey, "variable file") @@ -388,4 +396,7 @@ type contextOpts struct { // GetMode is the module.GetMode to use when loading the module tree. GetMode module.GetMode + + // Set to true when running a destroy plan/apply. + Destroy bool } diff --git a/command/plan.go b/command/plan.go index 24365d18538c..5c884d632a80 100644 --- a/command/plan.go +++ b/command/plan.go @@ -53,6 +53,7 @@ func (c *PlanCommand) Run(args []string) int { } ctx, _, err := c.Context(contextOpts{ + Destroy: destroy, Path: path, StatePath: c.Meta.statePath, }) @@ -86,7 +87,7 @@ func (c *PlanCommand) Run(args []string) int { } } - plan, err := ctx.Plan(&terraform.PlanOpts{Destroy: destroy}) + plan, err := ctx.Plan() if err != nil { c.Ui.Error(fmt.Sprintf("Error running plan: %s", err)) return 1 @@ -168,6 +169,10 @@ Options: up Terraform-managed resources. By default it will use the state "terraform.tfstate" if it exists. + -target=resource Resource to target. Operation will be limited to this + resource and its dependencies. This flag can be used + multiple times. + -var 'foo=bar' Set a variable in the Terraform configuration. This flag can be set multiple times. diff --git a/command/refresh.go b/command/refresh.go index 38d63005085d..32e7950474fa 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -135,6 +135,10 @@ Options: -state-out=path Path to write updated state file. By default, the "-state" path will be used. + -target=resource Resource to target. Operation will be limited to this + resource and its dependencies. This flag can be used + multiple times. + -var 'foo=bar' Set a variable in the Terraform configuration. This flag can be set multiple times. diff --git a/command/test-fixtures/apply-destroy-targeted/main.tf b/command/test-fixtures/apply-destroy-targeted/main.tf new file mode 100644 index 000000000000..45ebc5b970cc --- /dev/null +++ b/command/test-fixtures/apply-destroy-targeted/main.tf @@ -0,0 +1,7 @@ +resource "test_instance" "foo" { + count = 3 +} + +resource "test_load_balancer" "foo" { + instances = ["${test_instance.foo.*.id}"] +} diff --git a/dag/dag.go b/dag/dag.go index b81cb2874db2..0f53fb1f00b3 100644 --- a/dag/dag.go +++ b/dag/dag.go @@ -17,6 +17,40 @@ type AcyclicGraph struct { // WalkFunc is the callback used for walking the graph. type WalkFunc func(Vertex) error +// Returns a Set that includes every Vertex yielded by walking down from the +// provided starting Vertex v. +func (g *AcyclicGraph) Ancestors(v Vertex) (*Set, error) { + s := new(Set) + start := asVertexList(g.DownEdges(v)) + memoFunc := func(v Vertex) error { + s.Add(v) + return nil + } + + if err := g.depthFirstWalk(start, memoFunc); err != nil { + return nil, err + } + + return s, nil +} + +// Returns a Set that includes every Vertex yielded by walking up from the +// provided starting Vertex v. +func (g *AcyclicGraph) Descendents(v Vertex) (*Set, error) { + s := new(Set) + start := asVertexList(g.UpEdges(v)) + memoFunc := func(v Vertex) error { + s.Add(v) + return nil + } + + if err := g.reverseDepthFirstWalk(start, memoFunc); err != nil { + return nil, err + } + + return s, nil +} + // Root returns the root of the DAG, or an error. // // Complexity: O(V) @@ -61,15 +95,11 @@ func (g *AcyclicGraph) TransitiveReduction() { for _, u := range g.Vertices() { uTargets := g.DownEdges(u) - vs := make([]Vertex, uTargets.Len()) - for i, vRaw := range uTargets.List() { - vs[i] = vRaw.(Vertex) - } + vs := asVertexList(g.DownEdges(u)) g.depthFirstWalk(vs, func(v Vertex) error { shared := uTargets.Intersection(g.DownEdges(v)) - for _, raw := range shared.List() { - vPrime := raw.(Vertex) + for _, vPrime := range asVertexList(shared) { g.RemoveEdge(BasicEdge(u, vPrime)) } @@ -145,12 +175,10 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error { for _, v := range vertices { // Build our list of dependencies and the list of channels to // wait on until we start executing for this vertex. - depsRaw := g.DownEdges(v).List() - deps := make([]Vertex, len(depsRaw)) + deps := asVertexList(g.DownEdges(v)) depChs := make([]<-chan struct{}, len(deps)) - for i, raw := range depsRaw { - deps[i] = raw.(Vertex) - depChs[i] = vertMap[deps[i]] + for i, dep := range deps { + depChs[i] = vertMap[dep] } // Get our channel so that we can close it when we're done @@ -200,6 +228,16 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error { return errs } +// simple convenience helper for converting a dag.Set to a []Vertex +func asVertexList(s *Set) []Vertex { + rawList := s.List() + vertexList := make([]Vertex, len(rawList)) + for i, raw := range rawList { + vertexList[i] = raw.(Vertex) + } + return vertexList +} + // depthFirstWalk does a depth-first walk of the graph starting from // the vertices in start. This is not exported now but it would make sense // to export this publicly at some point. @@ -233,3 +271,36 @@ func (g *AcyclicGraph) depthFirstWalk(start []Vertex, cb WalkFunc) error { return nil } + +// reverseDepthFirstWalk does a depth-first walk _up_ the graph starting from +// the vertices in start. +func (g *AcyclicGraph) reverseDepthFirstWalk(start []Vertex, cb WalkFunc) error { + seen := make(map[Vertex]struct{}) + frontier := make([]Vertex, len(start)) + copy(frontier, start) + for len(frontier) > 0 { + // Pop the current vertex + n := len(frontier) + current := frontier[n-1] + frontier = frontier[:n-1] + + // Check if we've seen this already and return... + if _, ok := seen[current]; ok { + continue + } + seen[current] = struct{}{} + + // Visit the current node + if err := cb(current); err != nil { + return err + } + + // Visit targets of this in reverse order. + targets := g.UpEdges(current).List() + for i := len(targets) - 1; i >= 0; i-- { + frontier = append(frontier, targets[i].(Vertex)) + } + } + + return nil +} diff --git a/dag/dag_test.go b/dag/dag_test.go index feead7968a8c..e7b2db8d2264 100644 --- a/dag/dag_test.go +++ b/dag/dag_test.go @@ -126,6 +126,68 @@ func TestAcyclicGraphValidate_cycleSelf(t *testing.T) { } } +func TestAcyclicGraphAncestors(t *testing.T) { + var g AcyclicGraph + g.Add(1) + g.Add(2) + g.Add(3) + g.Add(4) + g.Add(5) + g.Connect(BasicEdge(0, 1)) + g.Connect(BasicEdge(1, 2)) + g.Connect(BasicEdge(2, 3)) + g.Connect(BasicEdge(3, 4)) + g.Connect(BasicEdge(4, 5)) + + actual, err := g.Ancestors(2) + if err != nil { + t.Fatalf("err: %#v", err) + } + + expected := []Vertex{3, 4, 5} + + if actual.Len() != len(expected) { + t.Fatalf("bad length! expected %#v to have len %d", actual, len(expected)) + } + + for _, e := range expected { + if !actual.Include(e) { + t.Fatalf("expected: %#v to include: %#v", expected, actual) + } + } +} + +func TestAcyclicGraphDescendents(t *testing.T) { + var g AcyclicGraph + g.Add(1) + g.Add(2) + g.Add(3) + g.Add(4) + g.Add(5) + g.Connect(BasicEdge(0, 1)) + g.Connect(BasicEdge(1, 2)) + g.Connect(BasicEdge(2, 3)) + g.Connect(BasicEdge(3, 4)) + g.Connect(BasicEdge(4, 5)) + + actual, err := g.Descendents(2) + if err != nil { + t.Fatalf("err: %#v", err) + } + + expected := []Vertex{0, 1} + + if actual.Len() != len(expected) { + t.Fatalf("bad length! expected %#v to have len %d", actual, len(expected)) + } + + for _, e := range expected { + if !actual.Include(e) { + t.Fatalf("expected: %#v to include: %#v", expected, actual) + } + } +} + func TestAcyclicGraphWalk(t *testing.T) { var g AcyclicGraph g.Add(1) diff --git a/helper/resource/testing.go b/helper/resource/testing.go index cedadfc72bd9..90cfc175fa92 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -190,6 +190,7 @@ func testStep( // Build the context opts.Module = mod opts.State = state + opts.Destroy = step.Destroy ctx := terraform.NewContext(&opts) if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { estrs := make([]string, len(es)) @@ -209,7 +210,7 @@ func testStep( } // Plan! - if p, err := ctx.Plan(&terraform.PlanOpts{Destroy: step.Destroy}); err != nil { + if p, err := ctx.Plan(); err != nil { return state, fmt.Errorf( "Error planning: %s", err) } else { diff --git a/terraform/context.go b/terraform/context.go index 86a8045486bf..6beaab6360d5 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -33,6 +33,7 @@ const ( // ContextOpts are the user-configurable options to create a context with // NewContext. type ContextOpts struct { + Destroy bool Diff *Diff Hooks []Hook Module *module.Tree @@ -40,6 +41,7 @@ type ContextOpts struct { State *State Providers map[string]ResourceProviderFactory Provisioners map[string]ResourceProvisionerFactory + Targets []string Variables map[string]string UIInput UIInput @@ -49,6 +51,7 @@ type ContextOpts struct { // perform operations on infrastructure. This structure is built using // NewContext. See the documentation for that. type Context struct { + destroy bool diff *Diff diffLock sync.RWMutex hooks []Hook @@ -58,6 +61,7 @@ type Context struct { sh *stopHook state *State stateLock sync.RWMutex + targets []string uiInput UIInput variables map[string]string @@ -95,12 +99,14 @@ func NewContext(opts *ContextOpts) *Context { } return &Context{ + destroy: opts.Destroy, diff: opts.Diff, hooks: hooks, module: opts.Module, providers: opts.Providers, provisioners: opts.Provisioners, state: state, + targets: opts.Targets, uiInput: opts.UIInput, variables: opts.Variables, @@ -135,6 +141,8 @@ func (c *Context) GraphBuilder() GraphBuilder { Providers: providers, Provisioners: provisioners, State: c.state, + Targets: c.targets, + Destroy: c.destroy, } } @@ -253,7 +261,7 @@ func (c *Context) Apply() (*State, error) { // // Plan also updates the diff of this context to be the diff generated // by the plan, so Apply can be called after. -func (c *Context) Plan(opts *PlanOpts) (*Plan, error) { +func (c *Context) Plan() (*Plan, error) { v := c.acquireRun() defer c.releaseRun(v) @@ -264,7 +272,7 @@ func (c *Context) Plan(opts *PlanOpts) (*Plan, error) { } var operation walkOperation - if opts != nil && opts.Destroy { + if c.destroy { operation = walkPlanDestroy } else { // Set our state to be something temporary. We do this so that diff --git a/terraform/context_test.go b/terraform/context_test.go index abffbf5c3e85..238f04d0459b 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -24,7 +24,7 @@ func TestContext2Plan(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -57,7 +57,7 @@ func TestContext2Plan_emptyDiff(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -80,7 +80,7 @@ func TestContext2Plan_minimal(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -103,7 +103,7 @@ func TestContext2Plan_modules(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -126,7 +126,7 @@ func TestContext2Plan_moduleInput(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -149,7 +149,7 @@ func TestContext2Plan_moduleInputComputed(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -175,7 +175,7 @@ func TestContext2Plan_moduleInputFromVar(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -198,7 +198,7 @@ func TestContext2Plan_moduleMultiVar(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -237,7 +237,7 @@ func TestContext2Plan_moduleOrphans(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -282,7 +282,7 @@ func TestContext2Plan_moduleProviderInherit(t *testing.T) { }, }) - _, err := ctx.Plan(nil) + _, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -332,7 +332,7 @@ func TestContext2Plan_moduleProviderDefaults(t *testing.T) { }, }) - _, err := ctx.Plan(nil) + _, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -385,7 +385,7 @@ func TestContext2Plan_moduleProviderDefaultsVar(t *testing.T) { }, }) - _, err := ctx.Plan(nil) + _, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -410,7 +410,7 @@ func TestContext2Plan_moduleVar(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -433,7 +433,7 @@ func TestContext2Plan_moduleVarComputed(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -471,7 +471,7 @@ func TestContext2Plan_nil(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -491,7 +491,7 @@ func TestContext2Plan_computed(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -514,7 +514,7 @@ func TestContext2Plan_computedList(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -537,7 +537,7 @@ func TestContext2Plan_count(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -564,7 +564,7 @@ func TestContext2Plan_countComputed(t *testing.T) { }, }) - _, err := ctx.Plan(nil) + _, err := ctx.Plan() if err == nil { t.Fatal("should error") } @@ -581,7 +581,7 @@ func TestContext2Plan_countIndex(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -604,7 +604,7 @@ func TestContext2Plan_countIndexZero(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -630,7 +630,7 @@ func TestContext2Plan_countVar(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -653,7 +653,7 @@ func TestContext2Plan_countZero(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -676,7 +676,7 @@ func TestContext2Plan_countOneIndex(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -731,7 +731,7 @@ func TestContext2Plan_countDecreaseToOne(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -774,7 +774,7 @@ func TestContext2Plan_countIncreaseFromNotSet(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -817,7 +817,7 @@ func TestContext2Plan_countIncreaseFromOne(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -875,7 +875,7 @@ func TestContext2Plan_countIncreaseFromOneCorrupted(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -917,10 +917,11 @@ func TestContext2Plan_destroy(t *testing.T) { Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), }, - State: s, + State: s, + Destroy: true, }) - plan, err := ctx.Plan(&PlanOpts{Destroy: true}) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -971,10 +972,11 @@ func TestContext2Plan_moduleDestroy(t *testing.T) { Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), }, - State: s, + State: s, + Destroy: true, }) - plan, err := ctx.Plan(&PlanOpts{Destroy: true}) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1020,10 +1022,11 @@ func TestContext2Plan_moduleDestroyMultivar(t *testing.T) { Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), }, - State: s, + State: s, + Destroy: true, }) - plan, err := ctx.Plan(&PlanOpts{Destroy: true}) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1051,7 +1054,7 @@ func TestContext2Plan_pathVar(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1118,7 +1121,7 @@ func TestContext2Plan_diffVar(t *testing.T) { }, nil } - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1143,7 +1146,7 @@ func TestContext2Plan_hook(t *testing.T) { }, }) - _, err := ctx.Plan(nil) + _, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1183,7 +1186,7 @@ func TestContext2Plan_orphan(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1221,7 +1224,7 @@ func TestContext2Plan_state(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1273,7 +1276,7 @@ func TestContext2Plan_taint(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1324,7 +1327,7 @@ func TestContext2Plan_multiple_taint(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1357,7 +1360,7 @@ func TestContext2Plan_provider(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -1377,7 +1380,7 @@ func TestContext2Plan_varMultiCountOne(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1399,7 +1402,7 @@ func TestContext2Plan_varListErr(t *testing.T) { }, }) - _, err := ctx.Plan(nil) + _, err := ctx.Plan() if err == nil { t.Fatal("should error") } @@ -2468,7 +2471,7 @@ func TestContext2Input(t *testing.T) { t.Fatalf("err: %s", err) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2513,7 +2516,7 @@ func TestContext2Input_provider(t *testing.T) { t.Fatalf("err: %s", err) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2590,7 +2593,7 @@ func TestContext2Input_providerId(t *testing.T) { t.Fatalf("err: %s", err) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2638,7 +2641,7 @@ func TestContext2Input_providerOnly(t *testing.T) { t.Fatalf("err: %s", err) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2693,7 +2696,7 @@ func TestContext2Input_providerVars(t *testing.T) { t.Fatalf("err: %s", err) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2741,7 +2744,7 @@ func TestContext2Input_varOnly(t *testing.T) { t.Fatalf("err: %s", err) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2787,7 +2790,7 @@ func TestContext2Input_varOnlyUnset(t *testing.T) { t.Fatalf("err: %s", err) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2815,7 +2818,7 @@ func TestContext2Apply(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2848,7 +2851,7 @@ func TestContext2Apply_emptyModule(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2896,7 +2899,7 @@ func TestContext2Apply_createBeforeDestroy(t *testing.T) { State: state, }) - if p, err := ctx.Plan(nil); err != nil { + if p, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } else { t.Logf(p.String()) @@ -2950,7 +2953,7 @@ func TestContext2Apply_createBeforeDestroyUpdate(t *testing.T) { State: state, }) - if p, err := ctx.Plan(nil); err != nil { + if p, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } else { t.Logf(p.String()) @@ -2985,7 +2988,7 @@ func TestContext2Apply_minimal(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3013,7 +3016,7 @@ func TestContext2Apply_badDiff(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3071,7 +3074,7 @@ func TestContext2Apply_cancel(t *testing.T) { }, nil } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3112,7 +3115,7 @@ func TestContext2Apply_compute(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3181,7 +3184,7 @@ func TestContext2Apply_countDecrease(t *testing.T) { State: s, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3241,7 +3244,7 @@ func TestContext2Apply_countDecreaseToOne(t *testing.T) { State: s, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3303,7 +3306,7 @@ func TestContext2Apply_countDecreaseToOneCorrupted(t *testing.T) { State: s, }) - if p, err := ctx.Plan(nil); err != nil { + if p, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } else { testStringMatch(t, p, testTerraformApplyCountDecToOneCorruptedPlanStr) @@ -3354,7 +3357,7 @@ func TestContext2Apply_countTainted(t *testing.T) { State: s, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3382,7 +3385,7 @@ func TestContext2Apply_countVariable(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3410,7 +3413,7 @@ func TestContext2Apply_module(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3439,9 +3442,10 @@ func TestContext2Apply_moduleVarResourceCount(t *testing.T) { Variables: map[string]string{ "count": "2", }, + Destroy: true, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3459,7 +3463,7 @@ func TestContext2Apply_moduleVarResourceCount(t *testing.T) { }, }) - if _, err := ctx.Plan(&PlanOpts{Destroy: true}); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3481,7 +3485,7 @@ func TestContext2Apply_moduleBool(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3515,7 +3519,7 @@ func TestContext2Apply_multiProvider(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3548,7 +3552,7 @@ func TestContext2Apply_nilDiff(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3588,7 +3592,7 @@ func TestContext2Apply_Provisioner_compute(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3633,7 +3637,7 @@ func TestContext2Apply_provisionerCreateFail(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3672,7 +3676,7 @@ func TestContext2Apply_provisionerCreateFailNoId(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3712,7 +3716,7 @@ func TestContext2Apply_provisionerFail(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3767,7 +3771,7 @@ func TestContext2Apply_provisionerFail_createBeforeDestroy(t *testing.T) { State: state, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3816,7 +3820,7 @@ func TestContext2Apply_error_createBeforeDestroy(t *testing.T) { } p.DiffFn = testDiffFn - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3874,7 +3878,7 @@ func TestContext2Apply_errorDestroy_createBeforeDestroy(t *testing.T) { } p.DiffFn = testDiffFn - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3931,7 +3935,7 @@ func TestContext2Apply_multiDepose_createBeforeDestroy(t *testing.T) { } } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3955,7 +3959,7 @@ aws_instance.web: (1 deposed) State: state, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3983,7 +3987,7 @@ aws_instance.web: (2 deposed) } createdInstanceId = "qux" - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } state, err = ctx.Apply() @@ -4005,7 +4009,7 @@ aws_instance.web: (1 deposed) } createdInstanceId = "quux" - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } state, err = ctx.Apply() @@ -4045,7 +4049,7 @@ func TestContext2Apply_provisionerResourceRef(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4091,7 +4095,7 @@ func TestContext2Apply_provisionerSelfRef(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4144,7 +4148,7 @@ func TestContext2Apply_provisionerMultiSelfRef(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4192,7 +4196,7 @@ func TestContext2Apply_Provisioner_Diff(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4229,7 +4233,7 @@ func TestContext2Apply_Provisioner_Diff(t *testing.T) { State: state, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4300,7 +4304,7 @@ func TestContext2Apply_outputDiffVars(t *testing.T) { }, nil } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } if _, err := ctx.Apply(); err != nil { @@ -4363,7 +4367,7 @@ func TestContext2Apply_Provisioner_ConnInfo(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4399,22 +4403,32 @@ func TestContext2Apply_destroy(t *testing.T) { }) // First plan and apply a create operation - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } - if _, err := ctx.Apply(); err != nil { + state, err := ctx.Apply() + if err != nil { t.Fatalf("err: %s", err) } // Next, plan and apply a destroy operation - if _, err := ctx.Plan(&PlanOpts{Destroy: true}); err != nil { + h.Active = true + ctx = testContext2(t, &ContextOpts{ + Destroy: true, + State: state, + Module: m, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } - h.Active = true - - state, err := ctx.Apply() + state, err = ctx.Apply() if err != nil { t.Fatalf("err: %s", err) } @@ -4430,7 +4444,7 @@ func TestContext2Apply_destroy(t *testing.T) { expected2 := []string{"aws_instance.bar", "aws_instance.foo"} actual2 := h.IDs if !reflect.DeepEqual(actual2, expected2) { - t.Fatalf("bad: %#v", actual2) + t.Fatalf("expected: %#v\n\ngot:%#v", expected2, actual2) } } @@ -4449,22 +4463,33 @@ func TestContext2Apply_destroyOutputs(t *testing.T) { }) // First plan and apply a create operation - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } - if _, err := ctx.Apply(); err != nil { + state, err := ctx.Apply() + + if err != nil { t.Fatalf("err: %s", err) } // Next, plan and apply a destroy operation - if _, err := ctx.Plan(&PlanOpts{Destroy: true}); err != nil { + h.Active = true + ctx = testContext2(t, &ContextOpts{ + Destroy: true, + State: state, + Module: m, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } - h.Active = true - - state, err := ctx.Apply() + state, err = ctx.Apply() if err != nil { t.Fatalf("err: %s", err) } @@ -4520,7 +4545,7 @@ func TestContext2Apply_destroyOrphan(t *testing.T) { }, nil } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4577,10 +4602,11 @@ func TestContext2Apply_destroyTaintedProvisioner(t *testing.T) { Provisioners: map[string]ResourceProvisionerFactory{ "shell": testProvisionerFuncFixed(pr), }, - State: s, + State: s, + Destroy: true, }) - if _, err := ctx.Plan(&PlanOpts{Destroy: true}); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4638,7 +4664,7 @@ func TestContext2Apply_error(t *testing.T) { }, nil } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4705,7 +4731,7 @@ func TestContext2Apply_errorPartial(t *testing.T) { }, nil } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4740,7 +4766,7 @@ func TestContext2Apply_hook(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4788,7 +4814,7 @@ func TestContext2Apply_idAttr(t *testing.T) { }, nil } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4822,7 +4848,7 @@ func TestContext2Apply_output(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4850,7 +4876,7 @@ func TestContext2Apply_outputInvalid(t *testing.T) { }, }) - _, err := ctx.Plan(nil) + _, err := ctx.Plan() if err == nil { t.Fatalf("err: %s", err) } @@ -4871,7 +4897,7 @@ func TestContext2Apply_outputList(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4899,7 +4925,7 @@ func TestContext2Apply_outputMulti(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4927,7 +4953,7 @@ func TestContext2Apply_outputMultiIndex(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4992,7 +5018,7 @@ func TestContext2Apply_taint(t *testing.T) { State: s, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -5057,7 +5083,7 @@ func TestContext2Apply_taintDep(t *testing.T) { State: s, }) - if p, err := ctx.Plan(nil); err != nil { + if p, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } else { t.Logf("plan: %s", p) @@ -5120,7 +5146,7 @@ func TestContext2Apply_taintDepRequiresNew(t *testing.T) { State: s, }) - if p, err := ctx.Plan(nil); err != nil { + if p, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } else { t.Logf("plan: %s", p) @@ -5150,7 +5176,7 @@ func TestContext2Apply_unknownAttribute(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -5190,7 +5216,7 @@ func TestContext2Apply_vars(t *testing.T) { t.Fatalf("bad: %s", e) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -5248,7 +5274,7 @@ func TestContext2Apply_createBefore_depends(t *testing.T) { State: state, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -5357,7 +5383,7 @@ func TestContext2Apply_singleDestroy(t *testing.T) { State: state, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } diff --git a/terraform/graph_builder.go b/terraform/graph_builder.go index 4d572695495e..d42f6bfe05fd 100644 --- a/terraform/graph_builder.go +++ b/terraform/graph_builder.go @@ -65,6 +65,13 @@ type BuiltinGraphBuilder struct { // Provisioners is the list of provisioners supported. Provisioners []string + + // Targets is the user-specified list of resources to target. + Targets []string + + // Destroy is set to true when we're in a `terraform destroy` or a + // `terraform plan -destroy` + Destroy bool } // Build builds the graph according to the steps returned by Steps. @@ -104,6 +111,10 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer { }, }, + // Optionally reduces the graph to a user-specified list of targets and + // their dependencies. + &TargetsTransformer{Targets: b.Targets, Destroy: b.Destroy}, + // Create the destruction nodes &DestroyTransformer{}, &CreateBeforeDestroyTransformer{}, diff --git a/terraform/plan.go b/terraform/plan.go index e73fde3832ea..715136edcfc3 100644 --- a/terraform/plan.go +++ b/terraform/plan.go @@ -18,15 +18,6 @@ func init() { gob.Register(make(map[string]string)) } -// PlanOpts are the options used to generate an execution plan for -// Terraform. -type PlanOpts struct { - // If set to true, then the generated plan will destroy all resources - // that are created. Otherwise, it will move towards the desired state - // specified in the configuration. - Destroy bool -} - // Plan represents a single Terraform execution plan, which contains // all the information necessary to make an infrastructure change. type Plan struct { diff --git a/terraform/test-fixtures/transform-targets-basic/main.tf b/terraform/test-fixtures/transform-targets-basic/main.tf new file mode 100644 index 000000000000..b845a1de69f8 --- /dev/null +++ b/terraform/test-fixtures/transform-targets-basic/main.tf @@ -0,0 +1,16 @@ +resource "aws_vpc" "me" {} + +resource "aws_subnet" "me" { + vpc_id = "${aws_vpc.me.id}" +} + +resource "aws_instance" "me" { + subnet_id = "${aws_subnet.me.id}" +} + +resource "aws_vpc" "notme" {} +resource "aws_subnet" "notme" {} +resource "aws_instance" "notme" {} +resource "aws_instance" "notmeeither" { + name = "${aws_instance.me.id}" +} diff --git a/terraform/test-fixtures/transform-targets-destroy/main.tf b/terraform/test-fixtures/transform-targets-destroy/main.tf new file mode 100644 index 000000000000..da99de43c81f --- /dev/null +++ b/terraform/test-fixtures/transform-targets-destroy/main.tf @@ -0,0 +1,18 @@ +resource "aws_vpc" "notme" {} + +resource "aws_subnet" "notme" { + vpc_id = "${aws_vpc.notme.id}" +} + +resource "aws_instance" "me" { + subnet_id = "${aws_subnet.notme.id}" +} + +resource "aws_instance" "notme" {} +resource "aws_instance" "metoo" { + name = "${aws_instance.me.id}" +} + +resource "aws_elb" "me" { + instances = "${aws_instance.me.*.id}" +} diff --git a/terraform/transform_targets.go b/terraform/transform_targets.go new file mode 100644 index 000000000000..0897ef7f9220 --- /dev/null +++ b/terraform/transform_targets.go @@ -0,0 +1,77 @@ +package terraform + +import "github.com/hashicorp/terraform/dag" + +// TargetsTransformer is a GraphTransformer that, when the user specifies a +// list of resources to target, limits the graph to only those resources and +// their dependencies. +type TargetsTransformer struct { + // List of targeted resource names specified by the user + Targets []string + + // Set to true when we're in a `terraform destroy` or a + // `terraform plan -destroy` + Destroy bool +} + +func (t *TargetsTransformer) Transform(g *Graph) error { + if len(t.Targets) > 0 { + targetedNodes, err := t.selectTargetedNodes(g) + if err != nil { + return err + } + + for _, v := range g.Vertices() { + if !targetedNodes.Include(v) { + g.Remove(v) + } + } + } + return nil +} + +func (t *TargetsTransformer) selectTargetedNodes(g *Graph) (*dag.Set, error) { + targetedNodes := new(dag.Set) + for _, v := range g.Vertices() { + // Keep all providers; they'll be pruned later if necessary + if r, ok := v.(GraphNodeProvider); ok { + targetedNodes.Add(r) + continue + } + + // For the remaining filter, we only care about Resources and their deps + r, ok := v.(*GraphNodeConfigResource) + if !ok { + continue + } + + if t.resourceIsTarget(r) { + targetedNodes.Add(r) + + var deps *dag.Set + var err error + if t.Destroy { + deps, err = g.Descendents(r) + } else { + deps, err = g.Ancestors(r) + } + if err != nil { + return nil, err + } + + for _, d := range deps.List() { + targetedNodes.Add(d) + } + } + } + return targetedNodes, nil +} + +func (t *TargetsTransformer) resourceIsTarget(r *GraphNodeConfigResource) bool { + for _, target := range t.Targets { + if target == r.Name() { + return true + } + } + return false +} diff --git a/terraform/transform_targets_test.go b/terraform/transform_targets_test.go new file mode 100644 index 000000000000..2daa72827e5b --- /dev/null +++ b/terraform/transform_targets_test.go @@ -0,0 +1,71 @@ +package terraform + +import ( + "strings" + "testing" +) + +func TestTargetsTransformer(t *testing.T) { + mod := testModule(t, "transform-targets-basic") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &TargetsTransformer{Targets: []string{"aws_instance.me"}} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(` +aws_instance.me + aws_subnet.me +aws_subnet.me + aws_vpc.me +aws_vpc.me + `) + if actual != expected { + t.Fatalf("bad:\n\nexpected:\n%s\n\ngot:\n%s\n", expected, actual) + } +} + +func TestTargetsTransformer_destroy(t *testing.T) { + mod := testModule(t, "transform-targets-destroy") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &TargetsTransformer{ + Targets: []string{"aws_instance.me"}, + Destroy: true, + } + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(` +aws_elb.me + aws_instance.me +aws_instance.me +aws_instance.metoo + aws_instance.me + `) + if actual != expected { + t.Fatalf("bad:\n\nexpected:\n%s\n\ngot:\n%s\n", expected, actual) + } +} From 23d005b0e8146be3e31731ff7f27a02d5148d90d Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Fri, 27 Mar 2015 17:24:15 -0500 Subject: [PATCH 16/59] core: docs for targeted operations --- website/source/docs/commands/apply.html.markdown | 3 +++ website/source/docs/commands/destroy.html.markdown | 6 ++++++ website/source/docs/commands/plan.html.markdown | 3 +++ website/source/docs/commands/refresh.html.markdown | 3 +++ 4 files changed, 15 insertions(+) diff --git a/website/source/docs/commands/apply.html.markdown b/website/source/docs/commands/apply.html.markdown index c7e2c27da476..6414f0741ec3 100644 --- a/website/source/docs/commands/apply.html.markdown +++ b/website/source/docs/commands/apply.html.markdown @@ -44,6 +44,9 @@ The command-line flags are all optional. The list of available flags are: * `-state-out=path` - Path to write updated state file. By default, the `-state` path will be used. +* `-target=resource` - Resource to target. Operation will be limited to this + resource and its dependencies. This flag can be used multiple times. + * `-var 'foo=bar'` - Set a variable in the Terraform configuration. This flag can be set multiple times. diff --git a/website/source/docs/commands/destroy.html.markdown b/website/source/docs/commands/destroy.html.markdown index 4ea84f880019..0a0f3a738b78 100644 --- a/website/source/docs/commands/destroy.html.markdown +++ b/website/source/docs/commands/destroy.html.markdown @@ -21,3 +21,9 @@ confirmation before destroying. This command accepts all the flags that the [apply command](/docs/commands/apply.html) accepts. If `-force` is set, then the destroy confirmation will not be shown. + +The `-target` flag, instead of affecting "dependencies" will instead also +destroy any resources that _depend on_ the target(s) specified. + +The behavior of any `terraform destroy` command can be previewed at any time +with an equivalent `terraform plan -destroy` command. diff --git a/website/source/docs/commands/plan.html.markdown b/website/source/docs/commands/plan.html.markdown index 14c10c5da3db..fe5e900906a1 100644 --- a/website/source/docs/commands/plan.html.markdown +++ b/website/source/docs/commands/plan.html.markdown @@ -45,6 +45,9 @@ The command-line flags are all optional. The list of available flags are: * `-state=path` - Path to the state file. Defaults to "terraform.tfstate". +* `-target=resource` - Resource to target. Operation will be limited to this + resource and its dependencies. This flag can be used multiple times. + * `-var 'foo=bar'` - Set a variable in the Terraform configuration. This flag can be set multiple times. diff --git a/website/source/docs/commands/refresh.html.markdown b/website/source/docs/commands/refresh.html.markdown index cc797ca387b1..6d15e1a40935 100644 --- a/website/source/docs/commands/refresh.html.markdown +++ b/website/source/docs/commands/refresh.html.markdown @@ -36,6 +36,9 @@ The command-line flags are all optional. The list of available flags are: * `-state-out=path` - Path to write updated state file. By default, the `-state` path will be used. +* `-target=resource` - Resource to target. Operation will be limited to this + resource and its dependencies. This flag can be used multiple times. + * `-var 'foo=bar'` - Set a variable in the Terraform configuration. This flag can be set multiple times. From 40ebfb5cccd27fa38d0f693e2d0b18ef58a0212e Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Mon, 30 Mar 2015 15:17:28 -0500 Subject: [PATCH 17/59] core: fill out context tests for targeted ops --- terraform/context_test.go | 164 ++++++++++++++++++ .../test-fixtures/apply-targeted/main.tf | 7 + terraform/test-fixtures/plan-targeted/main.tf | 7 + .../test-fixtures/refresh-targeted/main.tf | 8 + 4 files changed, 186 insertions(+) create mode 100644 terraform/test-fixtures/apply-targeted/main.tf create mode 100644 terraform/test-fixtures/plan-targeted/main.tf create mode 100644 terraform/test-fixtures/refresh-targeted/main.tf diff --git a/terraform/context_test.go b/terraform/context_test.go index 238f04d0459b..67abb101ae78 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -1339,6 +1339,40 @@ func TestContext2Plan_multiple_taint(t *testing.T) { } } +func TestContext2Plan_targeted(t *testing.T) { + m := testModule(t, "plan-targeted") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Targets: []string{"aws_instance.foo"}, + }) + + plan, err := ctx.Plan() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(` +DIFF: + +CREATE: aws_instance.foo + num: "" => "2" + type: "" => "aws_instance" + +STATE: + + + `) + if actual != expected { + t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual) + } +} + func TestContext2Plan_provider(t *testing.T) { m := testModule(t, "plan-provider") p := testProvider("aws") @@ -1460,6 +1494,47 @@ func TestContext2Refresh(t *testing.T) { } } +func TestContext2Refresh_targeted(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-targeted") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_vpc.metoo": resourceState("aws_vpc", "vpc-abc123"), + "aws_instance.notme": resourceState("aws_instance", "i-bcd345"), + "aws_instance.me": resourceState("aws_instance", "i-abc123"), + "aws_elb.meneither": resourceState("aws_elb", "lb-abc123"), + }, + }, + }, + }, + Targets: []string{"aws_instance.me"}, + }) + + refreshedResources := make([]string, 0, 2) + p.RefreshFn = func(i *InstanceInfo, is *InstanceState) (*InstanceState, error) { + refreshedResources = append(refreshedResources, i.Id) + return is, nil + } + + _, err := ctx.Refresh() + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := []string{"aws_vpc.metoo", "aws_instance.me"} + if !reflect.DeepEqual(refreshedResources, expected) { + t.Fatalf("expected: %#v, got: %#v", expected, refreshedResources) + } +} + func TestContext2Refresh_delete(t *testing.T) { p := testProvider("aws") m := testModule(t, "refresh-basic") @@ -5164,6 +5239,86 @@ func TestContext2Apply_taintDepRequiresNew(t *testing.T) { } } +func TestContext2Apply_targeted(t *testing.T) { + m := testModule(t, "apply-targeted") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Targets: []string{"aws_instance.foo"}, + }) + + if _, err := ctx.Plan(); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + mod := state.RootModule() + if len(mod.Resources) != 1 { + t.Fatalf("expected 1 resource, got: %#v", mod.Resources) + } + + checkStateString(t, state, ` +aws_instance.foo: + ID = foo + num = 2 + type = aws_instance + `) +} + +func TestContext2Apply_targetedDestroy(t *testing.T) { + m := testModule(t, "apply-targeted") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": resourceState("aws_instance", "i-bcd345"), + "aws_instance.bar": resourceState("aws_instance", "i-abc123"), + }, + }, + }, + }, + Targets: []string{"aws_instance.foo"}, + Destroy: true, + }) + + if _, err := ctx.Plan(); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + mod := state.RootModule() + if len(mod.Resources) != 1 { + t.Fatalf("expected 1 resource, got: %#v", mod.Resources) + } + + checkStateString(t, state, ` +aws_instance.bar: + ID = i-abc123 + `) +} + func TestContext2Apply_unknownAttribute(t *testing.T) { m := testModule(t, "apply-unknown") p := testProvider("aws") @@ -5553,6 +5708,15 @@ func checkStateString(t *testing.T, state *State, expected string) { } } +func resourceState(resourceType, resourceID string) *ResourceState { + return &ResourceState{ + Type: resourceType, + Primary: &InstanceState{ + ID: resourceID, + }, + } +} + const testContextGraph = ` root: root aws_instance.bar diff --git a/terraform/test-fixtures/apply-targeted/main.tf b/terraform/test-fixtures/apply-targeted/main.tf new file mode 100644 index 000000000000..b07fc97f4d46 --- /dev/null +++ b/terraform/test-fixtures/apply-targeted/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "foo" { + num = "2" +} + +resource "aws_instance" "bar" { + foo = "bar" +} diff --git a/terraform/test-fixtures/plan-targeted/main.tf b/terraform/test-fixtures/plan-targeted/main.tf new file mode 100644 index 000000000000..1b6cdae67b0e --- /dev/null +++ b/terraform/test-fixtures/plan-targeted/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "foo" { + num = "2" +} + +resource "aws_instance" "bar" { + foo = "${aws_instance.foo.num}" +} diff --git a/terraform/test-fixtures/refresh-targeted/main.tf b/terraform/test-fixtures/refresh-targeted/main.tf new file mode 100644 index 000000000000..3a76184647fc --- /dev/null +++ b/terraform/test-fixtures/refresh-targeted/main.tf @@ -0,0 +1,8 @@ +resource "aws_vpc" "metoo" {} +resource "aws_instance" "notme" { } +resource "aws_instance" "me" { + vpc_id = "${aws_vpc.metoo.id}" +} +resource "aws_elb" "meneither" { + instances = ["${aws_instance.me.*.id}"] +} From c6300d511c51aa33e53e127484332d852f92c4f4 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Mon, 30 Mar 2015 19:02:36 -0500 Subject: [PATCH 18/59] core: formalize resource addressing Only used in targets for now. The plan is to use this for interpolation as well. This allows us to target: * individual resources expanded by `count` using bracket / index notation. * deposed / tainted resources with an `InstanceType` field after name Docs to follow. --- terraform/context_test.go | 205 +++++++++++++++++ terraform/graph_builder.go | 6 +- terraform/graph_config_node.go | 45 +++- terraform/instancetype.go | 13 ++ terraform/instancetype_string.go | 16 ++ terraform/resource_address.go | 98 +++++++++ terraform/resource_address_test.go | 207 ++++++++++++++++++ .../apply-targeted-count/main.tf | 7 + .../refresh-targeted-count/main.tf | 9 + terraform/transform_orphan.go | 13 ++ terraform/transform_resource.go | 48 +++- terraform/transform_targets.go | 44 +++- 12 files changed, 696 insertions(+), 15 deletions(-) create mode 100644 terraform/instancetype.go create mode 100644 terraform/instancetype_string.go create mode 100644 terraform/resource_address.go create mode 100644 terraform/resource_address_test.go create mode 100644 terraform/test-fixtures/apply-targeted-count/main.tf create mode 100644 terraform/test-fixtures/refresh-targeted-count/main.tf diff --git a/terraform/context_test.go b/terraform/context_test.go index 67abb101ae78..f582ae679d70 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -1535,6 +1535,98 @@ func TestContext2Refresh_targeted(t *testing.T) { } } +func TestContext2Refresh_targetedCount(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-targeted-count") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_vpc.metoo": resourceState("aws_vpc", "vpc-abc123"), + "aws_instance.notme": resourceState("aws_instance", "i-bcd345"), + "aws_instance.me.0": resourceState("aws_instance", "i-abc123"), + "aws_instance.me.1": resourceState("aws_instance", "i-cde567"), + "aws_instance.me.2": resourceState("aws_instance", "i-cde789"), + "aws_elb.meneither": resourceState("aws_elb", "lb-abc123"), + }, + }, + }, + }, + Targets: []string{"aws_instance.me"}, + }) + + refreshedResources := make([]string, 0, 2) + p.RefreshFn = func(i *InstanceInfo, is *InstanceState) (*InstanceState, error) { + refreshedResources = append(refreshedResources, i.Id) + return is, nil + } + + _, err := ctx.Refresh() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Target didn't specify index, so we should get all our instances + expected := []string{ + "aws_vpc.metoo", + "aws_instance.me.0", + "aws_instance.me.1", + "aws_instance.me.2", + } + if !reflect.DeepEqual(refreshedResources, expected) { + t.Fatalf("expected: %#v, got: %#v", expected, refreshedResources) + } +} + +func TestContext2Refresh_targetedCountIndex(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-targeted-count") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_vpc.metoo": resourceState("aws_vpc", "vpc-abc123"), + "aws_instance.notme": resourceState("aws_instance", "i-bcd345"), + "aws_instance.me.0": resourceState("aws_instance", "i-abc123"), + "aws_instance.me.1": resourceState("aws_instance", "i-cde567"), + "aws_instance.me.2": resourceState("aws_instance", "i-cde789"), + "aws_elb.meneither": resourceState("aws_elb", "lb-abc123"), + }, + }, + }, + }, + Targets: []string{"aws_instance.me[0]"}, + }) + + refreshedResources := make([]string, 0, 2) + p.RefreshFn = func(i *InstanceInfo, is *InstanceState) (*InstanceState, error) { + refreshedResources = append(refreshedResources, i.Id) + return is, nil + } + + _, err := ctx.Refresh() + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := []string{"aws_vpc.metoo", "aws_instance.me.0"} + if !reflect.DeepEqual(refreshedResources, expected) { + t.Fatalf("expected: %#v, got: %#v", expected, refreshedResources) + } +} + func TestContext2Refresh_delete(t *testing.T) { p := testProvider("aws") m := testModule(t, "refresh-basic") @@ -5274,6 +5366,66 @@ aws_instance.foo: `) } +func TestContext2Apply_targetedCount(t *testing.T) { + m := testModule(t, "apply-targeted-count") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Targets: []string{"aws_instance.foo"}, + }) + + if _, err := ctx.Plan(); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + checkStateString(t, state, ` +aws_instance.foo.0: + ID = foo +aws_instance.foo.1: + ID = foo +aws_instance.foo.2: + ID = foo + `) +} + +func TestContext2Apply_targetedCountIndex(t *testing.T) { + m := testModule(t, "apply-targeted-count") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Targets: []string{"aws_instance.foo[1]"}, + }) + + if _, err := ctx.Plan(); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + checkStateString(t, state, ` +aws_instance.foo.1: + ID = foo + `) +} + func TestContext2Apply_targetedDestroy(t *testing.T) { m := testModule(t, "apply-targeted") p := testProvider("aws") @@ -5319,6 +5471,59 @@ aws_instance.bar: `) } +func TestContext2Apply_targetedDestroyCountIndex(t *testing.T) { + m := testModule(t, "apply-targeted-count") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo.0": resourceState("aws_instance", "i-bcd345"), + "aws_instance.foo.1": resourceState("aws_instance", "i-bcd345"), + "aws_instance.foo.2": resourceState("aws_instance", "i-bcd345"), + "aws_instance.bar.0": resourceState("aws_instance", "i-abc123"), + "aws_instance.bar.1": resourceState("aws_instance", "i-abc123"), + "aws_instance.bar.2": resourceState("aws_instance", "i-abc123"), + }, + }, + }, + }, + Targets: []string{ + "aws_instance.foo[2]", + "aws_instance.bar[1]", + }, + Destroy: true, + }) + + if _, err := ctx.Plan(); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + checkStateString(t, state, ` +aws_instance.bar.0: + ID = i-abc123 +aws_instance.bar.2: + ID = i-abc123 +aws_instance.foo.0: + ID = i-bcd345 +aws_instance.foo.1: + ID = i-bcd345 + `) +} + func TestContext2Apply_unknownAttribute(t *testing.T) { m := testModule(t, "apply-unknown") p := testProvider("aws") diff --git a/terraform/graph_builder.go b/terraform/graph_builder.go index d42f6bfe05fd..03c59f958bbf 100644 --- a/terraform/graph_builder.go +++ b/terraform/graph_builder.go @@ -89,7 +89,11 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer { return []GraphTransformer{ // Create all our resources from the configuration and state &ConfigTransformer{Module: b.Root}, - &OrphanTransformer{State: b.State, Module: b.Root}, + &OrphanTransformer{ + State: b.State, + Module: b.Root, + Targeting: (len(b.Targets) > 0), + }, // Provider-related transformations &MissingProviderTransformer{Providers: b.Providers}, diff --git a/terraform/graph_config_node.go b/terraform/graph_config_node.go index 625992f3f5a3..ddb96da2cc3b 100644 --- a/terraform/graph_config_node.go +++ b/terraform/graph_config_node.go @@ -21,6 +21,26 @@ type graphNodeConfig interface { GraphNodeDependent } +// GraphNodeAddressable is an interface that all graph nodes for the +// configuration graph need to implement in order to be be addressed / targeted +// properly. +type GraphNodeAddressable interface { + graphNodeConfig + + ResourceAddress() *ResourceAddress +} + +// GraphNodeTargetable is an interface for graph nodes to implement when they +// need to be told about incoming targets. This is useful for nodes that need +// to respect targets as they dynamically expand. Note that the list of targets +// provided will contain every target provided, and each implementing graph +// node must filter this list to targets considered relevant. +type GraphNodeTargetable interface { + GraphNodeAddressable + + SetTargets([]ResourceAddress) +} + // GraphNodeConfigModule represents a module within the configuration graph. type GraphNodeConfigModule struct { Path []string @@ -191,6 +211,9 @@ type GraphNodeConfigResource struct { // If this is set to anything other than destroyModeNone, then this // resource represents a resource that will be destroyed in some way. DestroyMode GraphNodeDestroyMode + + // Used during DynamicExpand to target indexes + Targets []ResourceAddress } func (n *GraphNodeConfigResource) DependableName() []string { @@ -279,6 +302,7 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error) steps = append(steps, &ResourceCountTransformer{ Resource: n.Resource, Destroy: n.DestroyMode != DestroyNone, + Targets: n.Targets, }) } @@ -289,8 +313,9 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error) // expand orphans, which have all the same semantics in a destroy // as a primary. steps = append(steps, &OrphanTransformer{ - State: state, - View: n.Resource.Id(), + State: state, + View: n.Resource.Id(), + Targeting: (len(n.Targets) > 0), }) steps = append(steps, &DeposedTransformer{ @@ -314,6 +339,22 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error) return b.Build(ctx.Path()) } +// GraphNodeAddressable impl. +func (n *GraphNodeConfigResource) ResourceAddress() *ResourceAddress { + return &ResourceAddress{ + // Indicates no specific index; will match on other three fields + Index: -1, + InstanceType: TypePrimary, + Name: n.Resource.Name, + Type: n.Resource.Type, + } +} + +// GraphNodeTargetable impl. +func (n *GraphNodeConfigResource) SetTargets(targets []ResourceAddress) { + n.Targets = targets +} + // GraphNodeEvalable impl. func (n *GraphNodeConfigResource) EvalTree() EvalNode { return &EvalSequence{ diff --git a/terraform/instancetype.go b/terraform/instancetype.go new file mode 100644 index 000000000000..08959717b979 --- /dev/null +++ b/terraform/instancetype.go @@ -0,0 +1,13 @@ +package terraform + +//go:generate stringer -type=InstanceType instancetype.go + +// InstanceType is an enum of the various types of instances store in the State +type InstanceType int + +const ( + TypeInvalid InstanceType = iota + TypePrimary + TypeTainted + TypeDeposed +) diff --git a/terraform/instancetype_string.go b/terraform/instancetype_string.go new file mode 100644 index 000000000000..fc8697644ae6 --- /dev/null +++ b/terraform/instancetype_string.go @@ -0,0 +1,16 @@ +// generated by stringer -type=InstanceType instancetype.go; DO NOT EDIT + +package terraform + +import "fmt" + +const _InstanceType_name = "TypeInvalidTypePrimaryTypeTaintedTypeDeposed" + +var _InstanceType_index = [...]uint8{0, 11, 22, 33, 44} + +func (i InstanceType) String() string { + if i < 0 || i+1 >= InstanceType(len(_InstanceType_index)) { + return fmt.Sprintf("InstanceType(%d)", i) + } + return _InstanceType_name[_InstanceType_index[i]:_InstanceType_index[i+1]] +} diff --git a/terraform/resource_address.go b/terraform/resource_address.go new file mode 100644 index 000000000000..b54a923d8847 --- /dev/null +++ b/terraform/resource_address.go @@ -0,0 +1,98 @@ +package terraform + +import ( + "fmt" + "regexp" + "strconv" +) + +// ResourceAddress is a way of identifying an individual resource (or, +// eventually, a subset of resources) within the state. It is used for Targets. +type ResourceAddress struct { + Index int + InstanceType InstanceType + Name string + Type string +} + +func ParseResourceAddress(s string) (*ResourceAddress, error) { + matches, err := tokenizeResourceAddress(s) + if err != nil { + return nil, err + } + resourceIndex := -1 + if matches["index"] != "" { + var err error + if resourceIndex, err = strconv.Atoi(matches["index"]); err != nil { + return nil, err + } + } + instanceType := TypePrimary + if matches["instance_type"] != "" { + var err error + if instanceType, err = ParseInstanceType(matches["instance_type"]); err != nil { + return nil, err + } + } + + return &ResourceAddress{ + Index: resourceIndex, + InstanceType: instanceType, + Name: matches["name"], + Type: matches["type"], + }, nil +} + +func (addr *ResourceAddress) Equals(raw interface{}) bool { + other, ok := raw.(*ResourceAddress) + if !ok { + return false + } + + indexMatch := (addr.Index == -1 || + other.Index == -1 || + addr.Index == other.Index) + + return (indexMatch && + addr.InstanceType == other.InstanceType && + addr.Name == other.Name && + addr.Type == other.Type) +} + +func ParseInstanceType(s string) (InstanceType, error) { + switch s { + case "primary": + return TypePrimary, nil + case "deposed": + return TypeDeposed, nil + case "tainted": + return TypeTainted, nil + default: + return TypeInvalid, fmt.Errorf("Unexpected value for InstanceType field: %q", s) + } +} + +func tokenizeResourceAddress(s string) (map[string]string, error) { + // Example of portions of the regexp below using the + // string "aws_instance.web.tainted[1]" + re := regexp.MustCompile(`\A` + + // "aws_instance" + `(?P\w+)\.` + + // "web" + `(?P\w+)` + + // "tainted" (optional, omission implies: "primary") + `(?:\.(?P\w+))?` + + // "1" (optional, omission implies: "0") + `(?:\[(?P\d+)\])?` + + `\z`) + groupNames := re.SubexpNames() + rawMatches := re.FindAllStringSubmatch(s, -1) + if len(rawMatches) != 1 { + return nil, fmt.Errorf("Problem parsing address: %q", s) + } + matches := make(map[string]string) + for i, m := range rawMatches[0] { + matches[groupNames[i]] = m + } + return matches, nil +} diff --git a/terraform/resource_address_test.go b/terraform/resource_address_test.go new file mode 100644 index 000000000000..2a8caa1f8f96 --- /dev/null +++ b/terraform/resource_address_test.go @@ -0,0 +1,207 @@ +package terraform + +import ( + "reflect" + "testing" +) + +func TestParseResourceAddress(t *testing.T) { + cases := map[string]struct { + Input string + Expected *ResourceAddress + }{ + "implicit primary, no specific index": { + Input: "aws_instance.foo", + Expected: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: -1, + }, + }, + "implicit primary, explicit index": { + Input: "aws_instance.foo[2]", + Expected: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: 2, + }, + }, + "explicit primary, explicit index": { + Input: "aws_instance.foo.primary[2]", + Expected: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: 2, + }, + }, + "tainted": { + Input: "aws_instance.foo.tainted", + Expected: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypeTainted, + Index: -1, + }, + }, + "deposed": { + Input: "aws_instance.foo.deposed", + Expected: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypeDeposed, + Index: -1, + }, + }, + } + + for tn, tc := range cases { + out, err := ParseResourceAddress(tc.Input) + if err != nil { + t.Fatalf("unexpected err: %#v", err) + } + + if !reflect.DeepEqual(out, tc.Expected) { + t.Fatalf("bad: %q\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.Expected, out) + } + } +} + +func TestResourceAddressEquals(t *testing.T) { + cases := map[string]struct { + Address *ResourceAddress + Other interface{} + Expect bool + }{ + "basic match": { + Address: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: 0, + }, + Other: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: 0, + }, + Expect: true, + }, + "address does not set index": { + Address: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: -1, + }, + Other: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: 3, + }, + Expect: true, + }, + "other does not set index": { + Address: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: 3, + }, + Other: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: -1, + }, + Expect: true, + }, + "neither sets index": { + Address: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: -1, + }, + Other: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: -1, + }, + Expect: true, + }, + "different type": { + Address: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: 0, + }, + Other: &ResourceAddress{ + Type: "aws_vpc", + Name: "foo", + InstanceType: TypePrimary, + Index: 0, + }, + Expect: false, + }, + "different name": { + Address: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: 0, + }, + Other: &ResourceAddress{ + Type: "aws_instance", + Name: "bar", + InstanceType: TypePrimary, + Index: 0, + }, + Expect: false, + }, + "different instance type": { + Address: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: 0, + }, + Other: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypeTainted, + Index: 0, + }, + Expect: false, + }, + "different index": { + Address: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: 0, + }, + Other: &ResourceAddress{ + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: 1, + }, + Expect: false, + }, + } + + for tn, tc := range cases { + actual := tc.Address.Equals(tc.Other) + if actual != tc.Expect { + t.Fatalf("%q: expected equals: %t, got %t for:\n%#v\n%#v", + tn, tc.Expect, actual, tc.Address, tc.Other) + } + } +} diff --git a/terraform/test-fixtures/apply-targeted-count/main.tf b/terraform/test-fixtures/apply-targeted-count/main.tf new file mode 100644 index 000000000000..cd861898f203 --- /dev/null +++ b/terraform/test-fixtures/apply-targeted-count/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "foo" { + count = 3 +} + +resource "aws_instance" "bar" { + count = 3 +} diff --git a/terraform/test-fixtures/refresh-targeted-count/main.tf b/terraform/test-fixtures/refresh-targeted-count/main.tf new file mode 100644 index 000000000000..f564b629c1ac --- /dev/null +++ b/terraform/test-fixtures/refresh-targeted-count/main.tf @@ -0,0 +1,9 @@ +resource "aws_vpc" "metoo" {} +resource "aws_instance" "notme" { } +resource "aws_instance" "me" { + vpc_id = "${aws_vpc.metoo.id}" + count = 3 +} +resource "aws_elb" "meneither" { + instances = ["${aws_instance.me.*.id}"] +} diff --git a/terraform/transform_orphan.go b/terraform/transform_orphan.go index e2a9c7dcd432..5de64c65c682 100644 --- a/terraform/transform_orphan.go +++ b/terraform/transform_orphan.go @@ -2,6 +2,7 @@ package terraform import ( "fmt" + "log" "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config/module" @@ -25,6 +26,11 @@ type OrphanTransformer struct { // using the graph path. Module *module.Tree + // Targets are user-specified resources to target. We need to be aware of + // these so we don't improperly identify orphans when they've just been + // filtered out of the graph via targeting. + Targeting bool + // View, if non-nil will set a view on the module state. View string } @@ -35,6 +41,13 @@ func (t *OrphanTransformer) Transform(g *Graph) error { return nil } + if t.Targeting { + log.Printf("Skipping orphan transformer because we have targets.") + // If we are in a run where we are targeting nodes, we won't process + // orphans for this run. + return nil + } + // Build up all our state representatives resourceRep := make(map[string]struct{}) for _, v := range g.Vertices() { diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go index 8c2a00c788e8..21774e953ea3 100644 --- a/terraform/transform_resource.go +++ b/terraform/transform_resource.go @@ -12,6 +12,7 @@ import ( type ResourceCountTransformer struct { Resource *config.Resource Destroy bool + Targets []ResourceAddress } func (t *ResourceCountTransformer) Transform(g *Graph) error { @@ -27,7 +28,7 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error { } // For each count, build and add the node - nodes := make([]dag.Vertex, count) + nodes := make([]dag.Vertex, 0, count) for i := 0; i < count; i++ { // Set the index. If our count is 1 we special case it so that // we handle the "resource.0" and "resource" boundary properly. @@ -49,9 +50,14 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error { } } + // Skip nodes if targeting excludes them + if !t.nodeIsTargeted(node) { + continue + } + // Add the node now - nodes[i] = node - g.Add(nodes[i]) + nodes = append(nodes, node) + g.Add(node) } // Make the dependency connections @@ -64,6 +70,25 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error { return nil } +func (t *ResourceCountTransformer) nodeIsTargeted(node dag.Vertex) bool { + // no targets specified, everything stays in the graph + if len(t.Targets) == 0 { + return true + } + addressable, ok := node.(GraphNodeAddressable) + if !ok { + return false + } + + addr := addressable.ResourceAddress() + for _, targetAddr := range t.Targets { + if targetAddr.Equals(addr) { + return true + } + } + return false +} + type graphNodeExpandedResource struct { Index int Resource *config.Resource @@ -77,6 +102,23 @@ func (n *graphNodeExpandedResource) Name() string { return fmt.Sprintf("%s #%d", n.Resource.Id(), n.Index) } +// GraphNodeAddressable impl. +func (n *graphNodeExpandedResource) ResourceAddress() *ResourceAddress { + // We want this to report the logical index properly, so we must undo the + // special case from the expand + index := n.Index + if index == -1 { + index = 0 + } + return &ResourceAddress{ + Index: index, + // TODO: kjkjkj + InstanceType: TypePrimary, + Name: n.Resource.Name, + Type: n.Resource.Type, + } +} + // GraphNodeDependable impl. func (n *graphNodeExpandedResource) DependableName() []string { return []string{ diff --git a/terraform/transform_targets.go b/terraform/transform_targets.go index 0897ef7f9220..29a6d53c6fbd 100644 --- a/terraform/transform_targets.go +++ b/terraform/transform_targets.go @@ -16,13 +16,20 @@ type TargetsTransformer struct { func (t *TargetsTransformer) Transform(g *Graph) error { if len(t.Targets) > 0 { - targetedNodes, err := t.selectTargetedNodes(g) + // TODO: duplicated in OrphanTransformer; pull up parsing earlier + addrs, err := t.parseTargetAddresses() + if err != nil { + return err + } + + targetedNodes, err := t.selectTargetedNodes(g, addrs) if err != nil { return err } for _, v := range g.Vertices() { - if !targetedNodes.Include(v) { + if targetedNodes.Include(v) { + } else { g.Remove(v) } } @@ -30,7 +37,20 @@ func (t *TargetsTransformer) Transform(g *Graph) error { return nil } -func (t *TargetsTransformer) selectTargetedNodes(g *Graph) (*dag.Set, error) { +func (t *TargetsTransformer) parseTargetAddresses() ([]ResourceAddress, error) { + addrs := make([]ResourceAddress, len(t.Targets)) + for i, target := range t.Targets { + ta, err := ParseResourceAddress(target) + if err != nil { + return nil, err + } + addrs[i] = *ta + } + return addrs, nil +} + +func (t *TargetsTransformer) selectTargetedNodes( + g *Graph, addrs []ResourceAddress) (*dag.Set, error) { targetedNodes := new(dag.Set) for _, v := range g.Vertices() { // Keep all providers; they'll be pruned later if necessary @@ -39,14 +59,18 @@ func (t *TargetsTransformer) selectTargetedNodes(g *Graph) (*dag.Set, error) { continue } - // For the remaining filter, we only care about Resources and their deps - r, ok := v.(*GraphNodeConfigResource) + // For the remaining filter, we only care about addressable nodes + r, ok := v.(GraphNodeAddressable) if !ok { continue } - if t.resourceIsTarget(r) { + if t.nodeIsTarget(r, addrs) { targetedNodes.Add(r) + // If the node would like to know about targets, tell it. + if n, ok := r.(GraphNodeTargetable); ok { + n.SetTargets(addrs) + } var deps *dag.Set var err error @@ -67,9 +91,11 @@ func (t *TargetsTransformer) selectTargetedNodes(g *Graph) (*dag.Set, error) { return targetedNodes, nil } -func (t *TargetsTransformer) resourceIsTarget(r *GraphNodeConfigResource) bool { - for _, target := range t.Targets { - if target == r.Name() { +func (t *TargetsTransformer) nodeIsTarget( + r GraphNodeAddressable, addrs []ResourceAddress) bool { + addr := r.ResourceAddress() + for _, targetAddr := range addrs { + if targetAddr.Equals(addr) { return true } } From 91a3d22a9f36b48348a1018e3518273576396b7c Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Tue, 31 Mar 2015 18:48:54 -0500 Subject: [PATCH 19/59] docs: resource addressing --- .../source/docs/commands/apply.html.markdown | 6 +- .../source/docs/commands/plan.html.markdown | 6 +- .../docs/commands/refresh.html.markdown | 6 +- .../resource-addressing.html.markdown | 57 +++++++++++++++++++ website/source/layouts/docs.erb | 4 ++ 5 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 website/source/docs/internals/resource-addressing.html.markdown diff --git a/website/source/docs/commands/apply.html.markdown b/website/source/docs/commands/apply.html.markdown index 6414f0741ec3..9bb5acdbff5f 100644 --- a/website/source/docs/commands/apply.html.markdown +++ b/website/source/docs/commands/apply.html.markdown @@ -44,8 +44,10 @@ The command-line flags are all optional. The list of available flags are: * `-state-out=path` - Path to write updated state file. By default, the `-state` path will be used. -* `-target=resource` - Resource to target. Operation will be limited to this - resource and its dependencies. This flag can be used multiple times. +* `-target=resource` - A [Resource + Address](/docs/internals/resource-addressing.html) to target. Operation will + be limited to this resource and its dependencies. This flag can be used + multiple times. * `-var 'foo=bar'` - Set a variable in the Terraform configuration. This flag can be set multiple times. diff --git a/website/source/docs/commands/plan.html.markdown b/website/source/docs/commands/plan.html.markdown index fe5e900906a1..e05c460ce80e 100644 --- a/website/source/docs/commands/plan.html.markdown +++ b/website/source/docs/commands/plan.html.markdown @@ -45,8 +45,10 @@ The command-line flags are all optional. The list of available flags are: * `-state=path` - Path to the state file. Defaults to "terraform.tfstate". -* `-target=resource` - Resource to target. Operation will be limited to this - resource and its dependencies. This flag can be used multiple times. +* `-target=resource` - A [Resource + Address](/docs/internals/resource-addressing.html) to target. Operation will + be limited to this resource and its dependencies. This flag can be used + multiple times. * `-var 'foo=bar'` - Set a variable in the Terraform configuration. This flag can be set multiple times. diff --git a/website/source/docs/commands/refresh.html.markdown b/website/source/docs/commands/refresh.html.markdown index 6d15e1a40935..0fc3fc9383ce 100644 --- a/website/source/docs/commands/refresh.html.markdown +++ b/website/source/docs/commands/refresh.html.markdown @@ -36,8 +36,10 @@ The command-line flags are all optional. The list of available flags are: * `-state-out=path` - Path to write updated state file. By default, the `-state` path will be used. -* `-target=resource` - Resource to target. Operation will be limited to this - resource and its dependencies. This flag can be used multiple times. +* `-target=resource` - A [Resource + Address](/docs/internals/resource-addressing.html) to target. Operation will + be limited to this resource and its dependencies. This flag can be used + multiple times. * `-var 'foo=bar'` - Set a variable in the Terraform configuration. This flag can be set multiple times. diff --git a/website/source/docs/internals/resource-addressing.html.markdown b/website/source/docs/internals/resource-addressing.html.markdown new file mode 100644 index 000000000000..b4b994a88af4 --- /dev/null +++ b/website/source/docs/internals/resource-addressing.html.markdown @@ -0,0 +1,57 @@ +--- +layout: "docs" +page_title: "Internals: Resource Address" +sidebar_current: "docs-internals-resource-addressing" +description: |- + Resource addressing is used to target specific resources in a larger + infrastructure. +--- + +# Resource Addressing + +A __Resource Address__ is a string that references a specific resource in a +larger infrastructure. The syntax of a resource address is: + +``` +.[optional fields] +``` + +Required fields: + + * `resource_type` - Type of the resource being addressed. + * `resource_name` - User-defined name of the resource. + +Optional fields may include: + + * `[N]` - where `N` is a `0`-based index into a resource with multiple + instances specified by the `count` meta-parameter. Omitting an index when + addressing a resource where `count > 1` means that the address references + all instances. + + +## Examples + +Given a Terraform config that includes: + +``` +resource "aws_instance" "web" { + # ... + count = 4 +} +``` + +An address like this: + + +``` +aws_instance.web[3] +``` + +Refers to only the last instance in the config, and an address like this: + +``` +aws_instance.web +``` + + +Refers to all four "web" instances. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index a0b31127a3f0..bbdecd4c8a04 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -215,6 +215,10 @@ > Resource Lifecycle + + > + Resource Addressing + From 141b40189e6ae5cba1903bc5c1691478c5ac02b5 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 1 Apr 2015 15:31:21 +0000 Subject: [PATCH 20/59] os-floating-ips support This commit causes the resource to manage floating IPs by way of the os-floating-ips API. At the moment, it works with both nova-network and Neutron environments, but if you use multiple Neutron networks, the network that supports the floating IP must be listed first. --- .../resource_openstack_compute_instance_v2.go | 112 +++--------------- 1 file changed, 16 insertions(+), 96 deletions(-) diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go index b5fe36a10666..42ffb8445d12 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go @@ -13,14 +13,13 @@ import ( "github.com/hashicorp/terraform/helper/schema" "github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip" "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups" "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach" "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" "github.com/rackspace/gophercloud/openstack/compute/v2/images" "github.com/rackspace/gophercloud/openstack/compute/v2/servers" - "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" - "github.com/rackspace/gophercloud/openstack/networking/v2/ports" "github.com/rackspace/gophercloud/pagination" ) @@ -76,7 +75,7 @@ func resourceComputeInstanceV2() *schema.Resource { Optional: true, ForceNew: false, }, - "user_data": &schema.Schema{ + "user_data": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, @@ -291,18 +290,8 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e } floatingIP := d.Get("floating_ip").(string) if floatingIP != "" { - networkingClient, err := config.networkingV2Client(d.Get("region").(string)) - if err != nil { - return fmt.Errorf("Error creating OpenStack compute client: %s", err) - } - - allFloatingIPs, err := getFloatingIPs(networkingClient) - if err != nil { - return fmt.Errorf("Error listing OpenStack floating IPs: %s", err) - } - err = assignFloatingIP(networkingClient, extractFloatingIPFromIP(allFloatingIPs, floatingIP), server.ID) - if err != nil { - return fmt.Errorf("Error assigning floating IP to OpenStack compute instance: %s", err) + if err := floatingip.Associate(computeClient, server.ID, floatingIP).ExtractErr(); err != nil { + return fmt.Errorf("Error associating floating IP: %s", err) } } @@ -553,20 +542,20 @@ func resourceComputeInstanceV2Update(d *schema.ResourceData, meta interface{}) e } if d.HasChange("floating_ip") { - floatingIP := d.Get("floating_ip").(string) - if floatingIP != "" { - networkingClient, err := config.networkingV2Client(d.Get("region").(string)) - if err != nil { - return fmt.Errorf("Error creating OpenStack compute client: %s", err) + oldFIP, newFIP := d.GetChange("floating_ip") + log.Printf("[DEBUG] Old Floating IP: %v", oldFIP) + log.Printf("[DEBUG] New Floating IP: %v", newFIP) + if oldFIP.(string) != "" { + log.Printf("[DEBUG] Attemping to disassociate %s from %s", oldFIP, d.Id()) + if err := floatingip.Disassociate(computeClient, d.Id(), oldFIP.(string)).ExtractErr(); err != nil { + return fmt.Errorf("Error disassociating Floating IP during update: %s", err) } + } - allFloatingIPs, err := getFloatingIPs(networkingClient) - if err != nil { - return fmt.Errorf("Error listing OpenStack floating IPs: %s", err) - } - err = assignFloatingIP(networkingClient, extractFloatingIPFromIP(allFloatingIPs, floatingIP), d.Id()) - if err != nil { - fmt.Errorf("Error assigning floating IP to OpenStack compute instance: %s", err) + if newFIP.(string) != "" { + log.Printf("[DEBUG] Attemping to associate %s to %s", newFIP, d.Id()) + if err := floatingip.Associate(computeClient, d.Id(), newFIP.(string)).ExtractErr(); err != nil { + return fmt.Errorf("Error associating Floating IP during update: %s", err) } } } @@ -759,75 +748,6 @@ func resourceInstanceBlockDeviceV2(d *schema.ResourceData, bd map[string]interfa return bfvOpts } -func extractFloatingIPFromIP(ips []floatingips.FloatingIP, IP string) *floatingips.FloatingIP { - for _, floatingIP := range ips { - if floatingIP.FloatingIP == IP { - return &floatingIP - } - } - return nil -} - -func assignFloatingIP(networkingClient *gophercloud.ServiceClient, floatingIP *floatingips.FloatingIP, instanceID string) error { - portID, err := getInstancePortID(networkingClient, instanceID) - if err != nil { - return err - } - return floatingips.Update(networkingClient, floatingIP.ID, floatingips.UpdateOpts{ - PortID: portID, - }).Err -} - -func getInstancePortID(networkingClient *gophercloud.ServiceClient, instanceID string) (string, error) { - pager := ports.List(networkingClient, ports.ListOpts{ - DeviceID: instanceID, - }) - - var portID string - err := pager.EachPage(func(page pagination.Page) (bool, error) { - portList, err := ports.ExtractPorts(page) - if err != nil { - return false, err - } - for _, port := range portList { - portID = port.ID - return false, nil - } - return true, nil - }) - - if err != nil { - return "", err - } - - if portID == "" { - return "", fmt.Errorf("Cannot find port for instance %s", instanceID) - } - - return portID, nil -} - -func getFloatingIPs(networkingClient *gophercloud.ServiceClient) ([]floatingips.FloatingIP, error) { - pager := floatingips.List(networkingClient, floatingips.ListOpts{}) - - ips := []floatingips.FloatingIP{} - err := pager.EachPage(func(page pagination.Page) (bool, error) { - floatingipList, err := floatingips.ExtractFloatingIPs(page) - if err != nil { - return false, err - } - for _, f := range floatingipList { - ips = append(ips, f) - } - return true, nil - }) - - if err != nil { - return nil, err - } - return ips, nil -} - func getImageID(client *gophercloud.ServiceClient, d *schema.ResourceData) (string, error) { imageId := d.Get("image_id").(string) From d768a01cabfae78b10585a505ae42f70ebda84ff Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sat, 21 Mar 2015 02:13:07 +0000 Subject: [PATCH 21/59] Removes check for a "public" network This is only possible if the OpenStack cloud explicitly has a network called "public". --- .../resource_openstack_compute_instance_v2.go | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go index 42ffb8445d12..11e0fd50aa07 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go @@ -331,20 +331,6 @@ func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) err d.Set("access_ip_v6", server.AccessIPv6) hostv4 := server.AccessIPv4 - if hostv4 == "" { - if publicAddressesRaw, ok := server.Addresses["public"]; ok { - publicAddresses := publicAddressesRaw.([]interface{}) - for _, paRaw := range publicAddresses { - pa := paRaw.(map[string]interface{}) - if pa["version"].(float64) == 4 { - hostv4 = pa["addr"].(string) - break - } - } - } - } - - // If no host found, just get the first IPv4 we find if hostv4 == "" { for _, networkAddresses := range server.Addresses { for _, element := range networkAddresses.([]interface{}) { @@ -360,20 +346,6 @@ func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) err log.Printf("hostv4: %s", hostv4) hostv6 := server.AccessIPv6 - if hostv6 == "" { - if publicAddressesRaw, ok := server.Addresses["public"]; ok { - publicAddresses := publicAddressesRaw.([]interface{}) - for _, paRaw := range publicAddresses { - pa := paRaw.(map[string]interface{}) - if pa["version"].(float64) == 6 { - hostv6 = fmt.Sprintf("[%s]", pa["addr"].(string)) - break - } - } - } - } - - // If no hostv6 found, just get the first IPv6 we find if hostv6 == "" { for _, networkAddresses := range server.Addresses { for _, element := range networkAddresses.([]interface{}) { From b90a6152c57743d00d0d61cd7aa22184773b04e9 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sat, 21 Mar 2015 02:16:46 +0000 Subject: [PATCH 22/59] Renamed fixed_ip to fixed_ip_v4 and added fixed_ip_v6 --- .../openstack/resource_openstack_compute_instance_v2.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go index 11e0fd50aa07..c9c4c7fb6ae2 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go @@ -115,7 +115,11 @@ func resourceComputeInstanceV2() *schema.Resource { Type: schema.TypeString, Optional: true, }, - "fixed_ip": &schema.Schema{ + "fixed_ip_v4": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "fixed_ip_v6": &schema.Schema{ Type: schema.TypeString, Optional: true, }, @@ -691,7 +695,7 @@ func resourceInstanceNetworksV2(d *schema.ResourceData) []servers.Network { networks[i] = servers.Network{ UUID: rawMap["uuid"].(string), Port: rawMap["port"].(string), - FixedIP: rawMap["fixed_ip"].(string), + FixedIP: rawMap["fixed_ip_v4"].(string), } } return networks From b160654cb30fd4b4c00ae796eb15ea2e5b62ef96 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sat, 21 Mar 2015 02:35:31 +0000 Subject: [PATCH 23/59] Allow networks to be specified by name This commit allows the user to specify a network by name rather than just uuid. This is done via the os-tenant-networks api extension. This works for both neutron and nova-network. --- .../resource_openstack_compute_instance_v2.go | 60 ++++++++++++++++--- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go index c9c4c7fb6ae2..a3f9e032bd87 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go @@ -16,6 +16,7 @@ import ( "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip" "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks" "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach" "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" "github.com/rackspace/gophercloud/openstack/compute/v2/images" @@ -111,6 +112,10 @@ func resourceComputeInstanceV2() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, "port": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -233,13 +238,27 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e return err } + networkDetails, err := resourceInstanceNetworks(computeClient, d) + if err != nil { + return err + } + + networks := make([]servers.Network, len(networkDetails)) + for i, net := range networkDetails { + networks[i] = servers.Network{ + UUID: net["uuid"].(string), + Port: net["port"].(string), + FixedIP: net["fixed_ip_v4"].(string), + } + } + createOpts = &servers.CreateOpts{ Name: d.Get("name").(string), ImageRef: imageId, FlavorRef: flavorId, SecurityGroups: resourceInstanceSecGroupsV2(d), AvailabilityZone: d.Get("availability_zone").(string), - Networks: resourceInstanceNetworksV2(d), + Networks: networks, Metadata: resourceInstanceMetadataV2(d), ConfigDrive: d.Get("config_drive").(bool), AdminPass: d.Get("admin_pass").(string), @@ -687,18 +706,43 @@ func resourceInstanceSecGroupsV2(d *schema.ResourceData) []string { return secgroups } -func resourceInstanceNetworksV2(d *schema.ResourceData) []servers.Network { +func resourceInstanceNetworks(computeClient *gophercloud.ServiceClient, d *schema.ResourceData) ([]map[string]interface{}, error) { rawNetworks := d.Get("network").([]interface{}) - networks := make([]servers.Network, len(rawNetworks)) + newNetworks := make([]map[string]interface{}, len(rawNetworks)) + var tenantnet tenantnetworks.Network + for i, raw := range rawNetworks { rawMap := raw.(map[string]interface{}) - networks[i] = servers.Network{ - UUID: rawMap["uuid"].(string), - Port: rawMap["port"].(string), - FixedIP: rawMap["fixed_ip_v4"].(string), + + allPages, err := tenantnetworks.List(computeClient).AllPages() + if err != nil { + return nil, err + } + + networkList, err := tenantnetworks.ExtractNetworks(allPages) + if err != nil { + return nil, err + } + + for _, network := range networkList { + if network.Name == rawMap["name"] { + tenantnet = network + } + if network.ID == rawMap["uuid"] { + tenantnet = network + } + } + + newNetworks[i] = map[string]interface{}{ + "uuid": tenantnet.ID, + "port": rawMap["port"].(string), + "fixed_ip_v4": rawMap["fixed_ip_v4"].(string), } } - return networks + + d.Set("network", newNetworks) + + return newNetworks, nil } func resourceInstanceMetadataV2(d *schema.ResourceData) map[string]string { From ccba6983701a521a0ed9b1deb174267f123c914f Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sat, 21 Mar 2015 03:12:22 +0000 Subject: [PATCH 24/59] Network Read changes This commit changes how the network info is read from OpenStack. It pulls all relevant information from server.Addresses and merges it with the available information from the networks parameters. The access_v4, access_v6, and floating IP information is then determined from the result. A MAC address parameter is also added since that information is available in server.Addresses. --- .../resource_openstack_compute_instance_v2.go | 115 +++++++++++++++--- 1 file changed, 96 insertions(+), 19 deletions(-) diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go index a3f9e032bd87..f0e42073682d 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go @@ -128,6 +128,10 @@ func resourceComputeInstanceV2() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "mac": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, }, }, }, @@ -350,39 +354,83 @@ func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) err log.Printf("[DEBUG] Retreived Server %s: %+v", d.Id(), server) d.Set("name", server.Name) + + // begin reading the network configuration d.Set("access_ip_v4", server.AccessIPv4) d.Set("access_ip_v6", server.AccessIPv6) - hostv4 := server.AccessIPv4 - if hostv4 == "" { - for _, networkAddresses := range server.Addresses { - for _, element := range networkAddresses.([]interface{}) { - address := element.(map[string]interface{}) - if address["version"].(float64) == 4 { - hostv4 = address["addr"].(string) - break + hostv6 := server.AccessIPv6 + + addresses := resourceInstanceAddresses(server.Addresses) + networkDetails, err := resourceInstanceNetworks(computeClient, d) + if err != nil { + return err + } + + // if there are no networkDetails, make networks at least a length of 1 + networkLength := 1 + if len(networkDetails) > 0 { + networkLength = len(networkDetails) + } + networks := make([]map[string]interface{}, networkLength) + + // Loop through all networks and addresses, + // merge relevant address details. + if len(networkDetails) == 0 { + for _, n := range addresses { + if floatingIP, ok := n["floating_ip"]; ok { + hostv4 = floatingIP.(string) + } else { + if hostv4 == "" && n["fixed_ip_v4"] != nil { + hostv4 = n["fixed_ip_v4"].(string) } } + + if hostv6 == "" && n["fixed_ip_v6"] != nil { + hostv6 = n["fixed_ip_v6"].(string) + } + + networks[0] = map[string]interface{}{ + "name": n, + "fixed_ip_v4": n["fixed_ip_v4"], + "fixed_ip_v6": n["fixed_ip_v6"], + "mac": n["mac"], + } } - } - d.Set("access_ip_v4", hostv4) - log.Printf("hostv4: %s", hostv4) + } else { + for i, net := range networkDetails { + n := addresses[net["name"].(string)] - hostv6 := server.AccessIPv6 - if hostv6 == "" { - for _, networkAddresses := range server.Addresses { - for _, element := range networkAddresses.([]interface{}) { - address := element.(map[string]interface{}) - if address["version"].(float64) == 6 { - hostv6 = fmt.Sprintf("[%s]", address["addr"].(string)) - break + if floatingIP, ok := n["floating_ip"]; ok { + hostv4 = floatingIP.(string) + } else { + if hostv4 == "" && n["fixed_ip_v4"] != nil { + hostv4 = n["fixed_ip_v4"].(string) } } + + if hostv6 == "" && n["fixed_ip_v6"] != nil { + hostv6 = n["fixed_ip_v6"].(string) + } + + networks[i] = map[string]interface{}{ + "uuid": networkDetails[i]["uuid"], + "name": networkDetails[i]["name"], + "port": networkDetails[i]["port"], + "fixed_ip_v4": n["fixed_ip_v4"], + "fixed_ip_v6": n["fixed_ip_v6"], + "mac": n["mac"], + } } } + + d.Set("network", networks) + d.Set("access_ip_v4", hostv4) d.Set("access_ip_v6", hostv6) + log.Printf("hostv4: %s", hostv4) log.Printf("hostv6: %s", hostv6) + // prefer the v6 address if no v4 address exists. preferredv := "" if hostv4 != "" { preferredv = hostv4 @@ -397,6 +445,7 @@ func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) err "host": preferredv, }) } + // end network configuration d.Set("metadata", server.Metadata) @@ -735,6 +784,7 @@ func resourceInstanceNetworks(computeClient *gophercloud.ServiceClient, d *schem newNetworks[i] = map[string]interface{}{ "uuid": tenantnet.ID, + "name": tenantnet.Name, "port": rawMap["port"].(string), "fixed_ip_v4": rawMap["fixed_ip_v4"].(string), } @@ -742,9 +792,36 @@ func resourceInstanceNetworks(computeClient *gophercloud.ServiceClient, d *schem d.Set("network", newNetworks) + log.Printf("[DEBUG] networks: %+v", newNetworks) + return newNetworks, nil } +func resourceInstanceAddresses(addresses map[string]interface{}) map[string]map[string]interface{} { + + addrs := make(map[string]map[string]interface{}) + for n, networkAddresses := range addresses { + addrs[n] = make(map[string]interface{}) + for _, element := range networkAddresses.([]interface{}) { + address := element.(map[string]interface{}) + if address["OS-EXT-IPS:type"] == "floating" { + addrs[n]["floating_ip"] = address["addr"] + } else { + if address["version"].(float64) == 4 { + addrs[n]["fixed_ip_v4"] = address["addr"].(string) + } else { + addrs[n]["fixed_ip_v6"] = fmt.Sprintf("[%s]", address["addr"].(string)) + } + } + addrs[n]["mac"] = address["OS-EXT-IPS-MAC:mac_addr"].(string) + } + } + + log.Printf("[DEBUG] Addresses: %+v", addresses) + + return addrs +} + func resourceInstanceMetadataV2(d *schema.ResourceData) map[string]string { m := make(map[string]string) for key, val := range d.Get("metadata").(map[string]interface{}) { From 0d77232196ec2b1bafb03643311ccb38f5513008 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Tue, 24 Mar 2015 13:24:04 +0000 Subject: [PATCH 25/59] Fixing computed parameters --- .../resource_openstack_compute_instance_v2.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go index f0e42073682d..b4ac115df726 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go @@ -111,26 +111,31 @@ func resourceComputeInstanceV2() *schema.Resource { "uuid": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, }, "name": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, }, "port": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, }, "fixed_ip_v4": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, }, "fixed_ip_v6": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, }, "mac": &schema.Schema{ Type: schema.TypeString, - Optional: true, + Computed: true, }, }, }, @@ -361,8 +366,8 @@ func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) err hostv4 := server.AccessIPv4 hostv6 := server.AccessIPv6 - addresses := resourceInstanceAddresses(server.Addresses) networkDetails, err := resourceInstanceNetworks(computeClient, d) + addresses := resourceInstanceAddresses(server.Addresses) if err != nil { return err } @@ -424,6 +429,8 @@ func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) err } } + log.Printf("[DEBUG] new networks: %+v", networks) + d.Set("network", networks) d.Set("access_ip_v4", hostv4) d.Set("access_ip_v6", hostv6) @@ -790,8 +797,6 @@ func resourceInstanceNetworks(computeClient *gophercloud.ServiceClient, d *schem } } - d.Set("network", newNetworks) - log.Printf("[DEBUG] networks: %+v", newNetworks) return newNetworks, nil From 30b0fc7489cd268afae1a328fb05726b8f8fa1d7 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 25 Mar 2015 16:47:19 +0000 Subject: [PATCH 26/59] Only attempt to get the MAC address if it exists. --- .../openstack/resource_openstack_compute_instance_v2.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go index b4ac115df726..84c31ad8d66d 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go @@ -818,7 +818,9 @@ func resourceInstanceAddresses(addresses map[string]interface{}) map[string]map[ addrs[n]["fixed_ip_v6"] = fmt.Sprintf("[%s]", address["addr"].(string)) } } - addrs[n]["mac"] = address["OS-EXT-IPS-MAC:mac_addr"].(string) + if mac, ok := address["OS-EXT-IPS-MAC:mac_addr"]; ok { + addrs[n]["mac"] = mac.(string) + } } } From bb81228205ea9002e1e59a9add1b78d9f4a20d23 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 1 Apr 2015 01:56:34 +0000 Subject: [PATCH 27/59] typo with netname when no networks are specified --- .../openstack/resource_openstack_compute_instance_v2.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go index 84c31ad8d66d..a3e7acec1f9d 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go @@ -382,7 +382,7 @@ func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) err // Loop through all networks and addresses, // merge relevant address details. if len(networkDetails) == 0 { - for _, n := range addresses { + for netName, n := range addresses { if floatingIP, ok := n["floating_ip"]; ok { hostv4 = floatingIP.(string) } else { @@ -396,7 +396,7 @@ func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) err } networks[0] = map[string]interface{}{ - "name": n, + "name": netName, "fixed_ip_v4": n["fixed_ip_v4"], "fixed_ip_v6": n["fixed_ip_v6"], "mac": n["mac"], From 67e33a7ac97dee558b1209b996d59f9c216386fe Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 1 Apr 2015 02:56:24 +0000 Subject: [PATCH 28/59] Updated compute_instance acceptance tests for floating IPs --- builtin/providers/openstack/provider_test.go | 5 ++ ...urce_openstack_compute_instance_v2_test.go | 48 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/builtin/providers/openstack/provider_test.go b/builtin/providers/openstack/provider_test.go index 7b3e65dd4302..16009d04da98 100644 --- a/builtin/providers/openstack/provider_test.go +++ b/builtin/providers/openstack/provider_test.go @@ -63,4 +63,9 @@ func testAccPreCheck(t *testing.T) { if v1 == "" && v2 == "" { t.Fatal("OS_FLAVOR_ID or OS_FLAVOR_NAME must be set for acceptance tests") } + + v = os.Getenv("OS_NETWORK_NAME") + if v == "" { + t.Fatal("OS_NETWORK_NAME must be set for acceptance tests") + } } diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go index f4c6c8557da6..5c528e906179 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go @@ -2,12 +2,14 @@ package openstack import ( "fmt" + "os" "testing" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip" "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach" "github.com/rackspace/gophercloud/openstack/compute/v2/servers" "github.com/rackspace/gophercloud/pagination" @@ -53,6 +55,40 @@ func TestAccComputeV2Instance_volumeAttach(t *testing.T) { }) } +func TestAccComputeV2Instance_floatingIPAttach(t *testing.T) { + var instance servers.Server + var fip floatingip.FloatingIP + var testAccComputeV2Instance_floatingIPAttach = fmt.Sprintf(` + resource "openstack_compute_floatingip_v2" "myip" { + } + + resource "openstack_compute_instance_v2" "foo" { + name = "terraform-test" + floating_ip = "${openstack_compute_floatingip_v2.myip.address}" + + network { + name = "%s" + } + }`, + os.Getenv("OS_NETWORK_NAME")) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2InstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2Instance_floatingIPAttach, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeV2FloatingIPExists(t, "openstack_compute_floatingip_v2.myip", &fip), + testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance), + testAccCheckComputeV2InstanceFloatingIPAttach(&instance, &fip), + ), + }, + }, + }) +} + func testAccCheckComputeV2InstanceDestroy(s *terraform.State) error { config := testAccProvider.Meta().(*Config) computeClient, err := config.computeV2Client(OS_REGION_NAME) @@ -159,6 +195,18 @@ func testAccCheckComputeV2InstanceVolumeAttachment( } } +func testAccCheckComputeV2InstanceFloatingIPAttach( + instance *servers.Server, fip *floatingip.FloatingIP) resource.TestCheckFunc { + return func(s *terraform.State) error { + if fip.InstanceID == instance.ID { + return nil + } + + return fmt.Errorf("Floating IP %s was not attached to instance %s", fip.ID, instance.ID) + + } +} + var testAccComputeV2Instance_basic = fmt.Sprintf(` resource "openstack_compute_instance_v2" "foo" { region = "%s" From 84e448de1a5c58c97c96a6d8c0ce59331eafb1d5 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Wed, 1 Apr 2015 10:42:53 -0500 Subject: [PATCH 29/59] Fix hashcode for ASG test --- builtin/providers/aws/resource_aws_autoscaling_group_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_autoscaling_group_test.go b/builtin/providers/aws/resource_aws_autoscaling_group_test.go index 661e71fe8def..09a4d73a6cf8 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group_test.go @@ -26,7 +26,7 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) { testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group), testAccCheckAWSAutoScalingGroupAttributes(&group), resource.TestCheckResourceAttr( - "aws_autoscaling_group.bar", "availability_zones.1807834199", "us-west-2a"), + "aws_autoscaling_group.bar", "availability_zones.2487133097", "us-west-2a"), resource.TestCheckResourceAttr( "aws_autoscaling_group.bar", "name", "foobar3-terraform-test"), resource.TestCheckResourceAttr( From 4244d0947ec6c3d842a1781a5e7c22ffddb0a459 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 1 Apr 2015 16:06:47 +0000 Subject: [PATCH 30/59] Making the network resource computable This allows the obtained network information to be successfully stored for environments that do not require a network resource to be specified. --- .../openstack/resource_openstack_compute_instance_v2.go | 1 + 1 file changed, 1 insertion(+) diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go index a3e7acec1f9d..36fe297a0fb2 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go @@ -106,6 +106,7 @@ func resourceComputeInstanceV2() *schema.Resource { Type: schema.TypeList, Optional: true, ForceNew: true, + Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "uuid": &schema.Schema{ From 816c4b475fd0f1a159229562b7b7c10ceaa518de Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Wed, 1 Apr 2015 11:11:19 -0500 Subject: [PATCH 31/59] core: [tests] fix order dependent test --- terraform/context_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terraform/context_test.go b/terraform/context_test.go index f582ae679d70..b9de8d79d1aa 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -1579,6 +1579,8 @@ func TestContext2Refresh_targetedCount(t *testing.T) { "aws_instance.me.1", "aws_instance.me.2", } + sort.Strings(expected) + sort.Strings(refreshedResources) if !reflect.DeepEqual(refreshedResources, expected) { t.Fatalf("expected: %#v, got: %#v", expected, refreshedResources) } From 78963fc3d97642bf0c5a200639bdb29fd1e8ae8a Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Wed, 1 Apr 2015 09:13:41 -0500 Subject: [PATCH 32/59] providers/aws: fix/improve RDS pointers handling * d.Set has a pointer nil check we can lean on * need to be a bit more conservative about nil checks on nested structs; (this fixes the RDS acceptance tests) /cc @fanhaf --- .../providers/aws/resource_aws_db_instance.go | 51 ++++++++++--------- .../aws/resource_aws_db_instance_test.go | 14 ++--- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go index 11b05c81be90..9bcf21898585 100644 --- a/builtin/providers/aws/resource_aws_db_instance.go +++ b/builtin/providers/aws/resource_aws_db_instance.go @@ -304,33 +304,38 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error { return nil } - if v.DBName != nil { - d.Set("name", *v.DBName) - } else { - d.Set("name", "") - } - d.Set("username", *v.MasterUsername) - d.Set("engine", *v.Engine) - d.Set("engine_version", *v.EngineVersion) - d.Set("allocated_storage", *v.AllocatedStorage) - d.Set("storage_type", *v.StorageType) - d.Set("instance_class", *v.DBInstanceClass) - d.Set("availability_zone", *v.AvailabilityZone) - d.Set("backup_retention_period", *v.BackupRetentionPeriod) - d.Set("backup_window", *v.PreferredBackupWindow) - d.Set("maintenance_window", *v.PreferredMaintenanceWindow) - d.Set("multi_az", *v.MultiAZ) - d.Set("port", *v.Endpoint.Port) - d.Set("db_subnet_group_name", *v.DBSubnetGroup.DBSubnetGroupName) + d.Set("name", v.DBName) + d.Set("username", v.MasterUsername) + d.Set("engine", v.Engine) + d.Set("engine_version", v.EngineVersion) + d.Set("allocated_storage", v.AllocatedStorage) + d.Set("storage_type", v.StorageType) + d.Set("instance_class", v.DBInstanceClass) + d.Set("availability_zone", v.AvailabilityZone) + d.Set("backup_retention_period", v.BackupRetentionPeriod) + d.Set("backup_window", v.PreferredBackupWindow) + d.Set("maintenance_window", v.PreferredMaintenanceWindow) + d.Set("multi_az", v.MultiAZ) + if v.DBSubnetGroup != nil { + d.Set("db_subnet_group_name", v.DBSubnetGroup.DBSubnetGroupName) + } if len(v.DBParameterGroups) > 0 { - d.Set("parameter_group_name", *v.DBParameterGroups[0].DBParameterGroupName) + d.Set("parameter_group_name", v.DBParameterGroups[0].DBParameterGroupName) + } + + if v.Endpoint != nil { + d.Set("port", v.Endpoint.Port) + d.Set("address", v.Endpoint.Address) + + if v.Endpoint.Address != nil && v.Endpoint.Port != nil { + d.Set("endpoint", + fmt.Sprintf("%s:%d", *v.Endpoint.Address, *v.Endpoint.Port)) + } } - d.Set("address", *v.Endpoint.Address) - d.Set("endpoint", fmt.Sprintf("%s:%d", *v.Endpoint.Address, *v.Endpoint.Port)) - d.Set("status", *v.DBInstanceStatus) - d.Set("storage_encrypted", *v.StorageEncrypted) + d.Set("status", v.DBInstanceStatus) + d.Set("storage_encrypted", v.StorageEncrypted) // Create an empty schema.Set to hold all vpc security group ids ids := &schema.Set{ diff --git a/builtin/providers/aws/resource_aws_db_instance_test.go b/builtin/providers/aws/resource_aws_db_instance_test.go index 3141990e664b..ba86d005ad99 100644 --- a/builtin/providers/aws/resource_aws_db_instance_test.go +++ b/builtin/providers/aws/resource_aws_db_instance_test.go @@ -2,7 +2,9 @@ package aws import ( "fmt" + "math/rand" "testing" + "time" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" @@ -24,8 +26,6 @@ func TestAccAWSDBInstance(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckAWSDBInstanceExists("aws_db_instance.bar", &v), testAccCheckAWSDBInstanceAttributes(&v), - resource.TestCheckResourceAttr( - "aws_db_instance.bar", "identifier", "foobarbaz-test-terraform"), resource.TestCheckResourceAttr( "aws_db_instance.bar", "allocated_storage", "10"), resource.TestCheckResourceAttr( @@ -133,9 +133,12 @@ func testAccCheckAWSDBInstanceExists(n string, v *rds.DBInstance) resource.TestC } } -const testAccAWSDBInstanceConfig = ` +// Database names cannot collide, and deletion takes so long, that making the +// name a bit random helps so able we can kill a test that's just waiting for a +// delete and not be blocked on kicking off another one. +var testAccAWSDBInstanceConfig = fmt.Sprintf(` resource "aws_db_instance" "bar" { - identifier = "foobarbaz-test-terraform" + identifier = "foobarbaz-test-terraform-%d" allocated_storage = 10 engine = "mysql" @@ -148,5 +151,4 @@ resource "aws_db_instance" "bar" { backup_retention_period = 0 parameter_group_name = "default.mysql5.6" -} -` +}`, rand.New(rand.NewSource(time.Now().UnixNano())).Int()) From 9a091ffa784e8149e94ac08d43e44e903d591e6b Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 1 Apr 2015 09:38:19 -0700 Subject: [PATCH 33/59] command: plan supports detailed exit code --- command/plan.go | 12 ++++- command/plan_test.go | 50 ++++++++++++++++++++ command/test-fixtures/plan-emptydiff/main.tf | 0 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 command/test-fixtures/plan-emptydiff/main.tf diff --git a/command/plan.go b/command/plan.go index 5c884d632a80..f23e1bb6e4f0 100644 --- a/command/plan.go +++ b/command/plan.go @@ -16,7 +16,7 @@ type PlanCommand struct { } func (c *PlanCommand) Run(args []string) int { - var destroy, refresh bool + var destroy, refresh, detailed bool var outPath string var moduleDepth int @@ -29,6 +29,7 @@ func (c *PlanCommand) Run(args []string) int { cmdFlags.StringVar(&outPath, "out", "", "path") cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") + cmdFlags.BoolVar(&detailed, "detailed-exitcode", false, "detailed-exitcode") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 @@ -129,6 +130,9 @@ func (c *PlanCommand) Run(args []string) int { ModuleDepth: moduleDepth, })) + if detailed { + return 2 + } return 0 } @@ -152,6 +156,12 @@ Options: -destroy If set, a plan will be generated to destroy all resources managed by the given configuration and state. + -detailed-exitcode Return detailed exit codes when the command exits. This + will change the meaning of exit codes to: + 0 - Succeeded, diff is empty (no changes) + 1 - Errored + 2 - Succeeded, there is a diff + -input=true Ask for input for variables if not directly set. -module-depth=n Specifies the depth of modules to show in the output. diff --git a/command/plan_test.go b/command/plan_test.go index d981c2294e30..3455fbbc6178 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -567,6 +567,56 @@ func TestPlan_disableBackup(t *testing.T) { } } +func TestPlan_detailedExitcode(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(testFixturePath("plan")); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + p := testProvider() + ui := new(cli.MockUi) + c := &PlanCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{"-detailed-exitcode"} + if code := c.Run(args); code != 2 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} + +func TestPlan_detailedExitcode_emptyDiff(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(testFixturePath("plan-emptydiff")); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + p := testProvider() + ui := new(cli.MockUi) + c := &PlanCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{"-detailed-exitcode"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} + const planVarFile = ` foo = "bar" ` diff --git a/command/test-fixtures/plan-emptydiff/main.tf b/command/test-fixtures/plan-emptydiff/main.tf new file mode 100644 index 000000000000..e69de29bb2d1 From 4df04e0878b223ddf987d1f0d2b49af317363c5c Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 1 Apr 2015 09:49:36 -0700 Subject: [PATCH 34/59] website: document detailed exit codes for plan --- website/source/docs/commands/plan.html.markdown | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/website/source/docs/commands/plan.html.markdown b/website/source/docs/commands/plan.html.markdown index e05c460ce80e..1c0b1b68ac31 100644 --- a/website/source/docs/commands/plan.html.markdown +++ b/website/source/docs/commands/plan.html.markdown @@ -28,6 +28,13 @@ The command-line flags are all optional. The list of available flags are: * `-destroy` - If set, generates a plan to destroy all the known resources. +* `-detailed-exitcode` - Return a detailed exit code when the command exits. + When provided, this argument changes the exit codes and their meanings to + provide more granular information about what the resulting plan contains: + * 0 = Succeeded with empty diff (no changes) + * 1 = Error + * 2 = Succeeded with non-empty diff (changes present) + * `-input=true` - Ask for input for variables if not directly set. * `-module-depth=n` - Specifies the depth of modules to show in the output. From 815b79753a21582d124642f44502f73e96bdd277 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Wed, 1 Apr 2015 14:49:50 -0500 Subject: [PATCH 35/59] return error if failed to set tags on Route 53 zone --- builtin/providers/aws/resource_aws_route53_zone.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_route53_zone.go b/builtin/providers/aws/resource_aws_route53_zone.go index e6c8be5715ee..b60c91a799d2 100644 --- a/builtin/providers/aws/resource_aws_route53_zone.go +++ b/builtin/providers/aws/resource_aws_route53_zone.go @@ -105,7 +105,10 @@ func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error if resp.ResourceTagSet != nil { tags = resp.ResourceTagSet.Tags } - d.Set("tags", tagsToMapR53(tags)) + + if err := d.Set("tags", tagsToMapR53(tags)); err != nil { + return err + } return nil } From b31a69fe43dd3525ff87313ddef87dc206b3c999 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Wed, 1 Apr 2015 16:05:19 -0500 Subject: [PATCH 36/59] provider/aws: Allow DB Parameter group to change in RDS --- builtin/providers/aws/resource_aws_db_instance.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go index 0cf2e420280f..4b90b79baa7c 100644 --- a/builtin/providers/aws/resource_aws_db_instance.go +++ b/builtin/providers/aws/resource_aws_db_instance.go @@ -170,7 +170,6 @@ func resourceAwsDbInstance() *schema.Resource { Type: schema.TypeString, Optional: true, Computed: true, - ForceNew: true, }, "address": &schema.Schema{ @@ -456,6 +455,12 @@ func resourceAwsDbInstanceUpdate(d *schema.ResourceData, meta interface{}) error req.MultiAZ = aws.Boolean(d.Get("multi_az").(bool)) } + if d.HasChange("parameter_group_name") { + change = true + d.SetPartial("parameter_group_name") + req.DBParameterGroupName = aws.String(d.Get("parameter_group_name").(string)) + } + if change { log.Printf("[DEBUG] DB Instance Modification request: %#v", req) _, err := conn.ModifyDBInstance(req) From e2f3b0753598af8f66d59f1898978e45a79607a7 Mon Sep 17 00:00:00 2001 From: Clint Date: Wed, 1 Apr 2015 16:11:27 -0500 Subject: [PATCH 37/59] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24336b5ee3d5..19332c01d5fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,9 @@ IMPROVEMENTS: `www` instead of `www.example.com`. * providers/aws: Improve dependency violation error handling, when deleting Internet Gateways or Auto Scaling groups [GH-1325]. + * provider/aws: Add non-destructive updates to AWS RDS. You can now upgrade + `egine_version`, `parameter_group_name`, and `multi_az` without forcing + a new database to be created.[GH-1341] BUG FIXES: From ef4e03a7293f7727c8d902c5245be5f49c581b6e Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 1 Apr 2015 21:31:55 +0000 Subject: [PATCH 38/59] Workaround for missing tenant-network This commit resolves an issue where the tenant-network api extension does not exist. The caveat is that the user must either specify no networks (single network environment) or can only specify UUIDs for network configurations. --- .../resource_openstack_compute_instance_v2.go | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go index 36fe297a0fb2..a6cf8a597860 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go @@ -768,31 +768,51 @@ func resourceInstanceNetworks(computeClient *gophercloud.ServiceClient, d *schem newNetworks := make([]map[string]interface{}, len(rawNetworks)) var tenantnet tenantnetworks.Network + tenantNetworkExt := true for i, raw := range rawNetworks { rawMap := raw.(map[string]interface{}) allPages, err := tenantnetworks.List(computeClient).AllPages() if err != nil { - return nil, err - } + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return nil, err + } - networkList, err := tenantnetworks.ExtractNetworks(allPages) - if err != nil { - return nil, err + if errCode.Actual == 404 { + tenantNetworkExt = false + } else { + return nil, err + } } - for _, network := range networkList { - if network.Name == rawMap["name"] { - tenantnet = network + networkID := "" + networkName := "" + if tenantNetworkExt { + networkList, err := tenantnetworks.ExtractNetworks(allPages) + if err != nil { + return nil, err } - if network.ID == rawMap["uuid"] { - tenantnet = network + + for _, network := range networkList { + if network.Name == rawMap["name"] { + tenantnet = network + } + if network.ID == rawMap["uuid"] { + tenantnet = network + } } + + networkID = tenantnet.ID + networkName = tenantnet.Name + } else { + networkID = rawMap["uuid"].(string) + networkName = rawMap["name"].(string) } newNetworks[i] = map[string]interface{}{ - "uuid": tenantnet.ID, - "name": tenantnet.Name, + "uuid": networkID, + "name": networkName, "port": rawMap["port"].(string), "fixed_ip_v4": rawMap["fixed_ip_v4"].(string), } From 99ac8b44102126081c97fe8cdd842a104de9fb3f Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 1 Apr 2015 21:39:54 +0000 Subject: [PATCH 39/59] Compute Floating IP Test Update Changes the test to require a network UUID rather than a name. --- builtin/providers/openstack/provider_test.go | 4 ++-- .../openstack/resource_openstack_compute_instance_v2_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/builtin/providers/openstack/provider_test.go b/builtin/providers/openstack/provider_test.go index 16009d04da98..7e4b6d99fef2 100644 --- a/builtin/providers/openstack/provider_test.go +++ b/builtin/providers/openstack/provider_test.go @@ -64,8 +64,8 @@ func testAccPreCheck(t *testing.T) { t.Fatal("OS_FLAVOR_ID or OS_FLAVOR_NAME must be set for acceptance tests") } - v = os.Getenv("OS_NETWORK_NAME") + v = os.Getenv("OS_NETWORK_ID") if v == "" { - t.Fatal("OS_NETWORK_NAME must be set for acceptance tests") + t.Fatal("OS_NETWORK_ID must be set for acceptance tests") } } diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go index 5c528e906179..79bef72962e4 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go @@ -67,10 +67,10 @@ func TestAccComputeV2Instance_floatingIPAttach(t *testing.T) { floating_ip = "${openstack_compute_floatingip_v2.myip.address}" network { - name = "%s" + uuid = "%s" } }`, - os.Getenv("OS_NETWORK_NAME")) + os.Getenv("OS_NETWORK_ID")) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, From 7ca97f4bfcc97aff7b709000919449b5a3b4cd49 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 1 Apr 2015 22:54:09 +0000 Subject: [PATCH 40/59] Updating Floating IP acceptance tests --- ...ce_openstack_compute_floatingip_v2_test.go | 41 ++++++++++-- ...openstack_networking_floatingip_v2_test.go | 63 +++++++++++++++++-- 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/builtin/providers/openstack/resource_openstack_compute_floatingip_v2_test.go b/builtin/providers/openstack/resource_openstack_compute_floatingip_v2_test.go index a298a87d1854..d6fe43b529a9 100644 --- a/builtin/providers/openstack/resource_openstack_compute_floatingip_v2_test.go +++ b/builtin/providers/openstack/resource_openstack_compute_floatingip_v2_test.go @@ -2,12 +2,14 @@ package openstack import ( "fmt" + "os" "testing" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" ) func TestAccComputeV2FloatingIP_basic(t *testing.T) { @@ -28,6 +30,40 @@ func TestAccComputeV2FloatingIP_basic(t *testing.T) { }) } +func TestAccComputeV2FloatingIP_attach(t *testing.T) { + var instance servers.Server + var fip floatingip.FloatingIP + var testAccComputeV2FloatingIP_attach = fmt.Sprintf(` + resource "openstack_compute_floatingip_v2" "myip" { + } + + resource "openstack_compute_instance_v2" "foo" { + name = "terraform-test" + floating_ip = "${openstack_compute_floatingip_v2.myip.address}" + + network { + uuid = "%s" + } + }`, + os.Getenv("OS_NETWORK_ID")) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2FloatingIPDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2FloatingIP_attach, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeV2FloatingIPExists(t, "openstack_compute_floatingip_v2.myip", &fip), + testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance), + testAccCheckComputeV2InstanceFloatingIPAttach(&instance, &fip), + ), + }, + }, + }) +} + func testAccCheckComputeV2FloatingIPDestroy(s *terraform.State) error { config := testAccProvider.Meta().(*Config) computeClient, err := config.computeV2Client(OS_REGION_NAME) @@ -83,9 +119,4 @@ func testAccCheckComputeV2FloatingIPExists(t *testing.T, n string, kp *floatingi var testAccComputeV2FloatingIP_basic = ` resource "openstack_compute_floatingip_v2" "foo" { - } - - resource "openstack_compute_instance_v2" "bar" { - name = "terraform-acc-floating-ip-test" - floating_ip = "${openstack_compute_floatingip_v2.foo.address}" }` diff --git a/builtin/providers/openstack/resource_openstack_networking_floatingip_v2_test.go b/builtin/providers/openstack/resource_openstack_networking_floatingip_v2_test.go index 5c8ae38e3752..a989f2774dbe 100644 --- a/builtin/providers/openstack/resource_openstack_networking_floatingip_v2_test.go +++ b/builtin/providers/openstack/resource_openstack_networking_floatingip_v2_test.go @@ -2,11 +2,13 @@ package openstack import ( "fmt" + "os" "testing" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" ) @@ -28,6 +30,40 @@ func TestAccNetworkingV2FloatingIP_basic(t *testing.T) { }) } +func TestAccNetworkingV2FloatingIP_attach(t *testing.T) { + var instance servers.Server + var fip floatingips.FloatingIP + var testAccNetworkV2FloatingIP_attach = fmt.Sprintf(` + resource "openstack_networking_floatingip_v2" "myip" { + } + + resource "openstack_compute_instance_v2" "foo" { + name = "terraform-test" + floating_ip = "${openstack_networking_floatingip_v2.myip.address}" + + network { + uuid = "%s" + } + }`, + os.Getenv("OS_NETWORK_ID")) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckNetworkingV2FloatingIPDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccNetworkV2FloatingIP_attach, + Check: resource.ComposeTestCheckFunc( + testAccCheckNetworkingV2FloatingIPExists(t, "openstack_networking_floatingip_v2.myip", &fip), + testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance), + testAccCheckNetworkingV2InstanceFloatingIPAttach(&instance, &fip), + ), + }, + }, + }) +} + func testAccCheckNetworkingV2FloatingIPDestroy(s *terraform.State) error { config := testAccProvider.Meta().(*Config) networkClient, err := config.networkingV2Client(OS_REGION_NAME) @@ -81,11 +117,28 @@ func testAccCheckNetworkingV2FloatingIPExists(t *testing.T, n string, kp *floati } } +func testAccCheckNetworkingV2InstanceFloatingIPAttach( + instance *servers.Server, fip *floatingips.FloatingIP) resource.TestCheckFunc { + + // When Neutron is used, the Instance sometimes does not know its floating IP until some time + // after the attachment happened. This can be anywhere from 2-20 seconds. Because of that delay, + // the test usually completes with failure. + // However, the Fixed IP is known on both sides immediately, so that can be used as a bridge + // to ensure the two are now related. + // I think a better option is to introduce some state changing config in the actual resource. + return func(s *terraform.State) error { + for _, networkAddresses := range instance.Addresses { + for _, element := range networkAddresses.([]interface{}) { + address := element.(map[string]interface{}) + if address["OS-EXT-IPS:type"] == "fixed" && address["addr"] == fip.FixedIP { + return nil + } + } + } + return fmt.Errorf("Floating IP %+v was not attached to instance %+v", fip, instance) + } +} + var testAccNetworkingV2FloatingIP_basic = ` resource "openstack_networking_floatingip_v2" "foo" { - } - - resource "openstack_compute_instance_v2" "bar" { - name = "terraform-acc-floating-ip-test" - floating_ip = "${openstack_networking_floatingip_v2.foo.address}" }` From 1693767922e783d5792f54872121ea4005d10596 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Thu, 2 Apr 2015 00:10:46 +0000 Subject: [PATCH 41/59] Compute Instance basic acceptance test A change was made to account for clouds with multiple networks. --- ...urce_openstack_compute_instance_v2_test.go | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go index 79bef72962e4..587df56520d5 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go @@ -17,6 +17,17 @@ import ( func TestAccComputeV2Instance_basic(t *testing.T) { var instance servers.Server + var testAccComputeV2Instance_basic = fmt.Sprintf(` + resource "openstack_compute_instance_v2" "foo" { + name = "terraform-test" + network { + uuid = "%s" + } + metadata { + foo = "bar" + } + }`, + os.Getenv("OS_NETWORK_ID")) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -207,16 +218,6 @@ func testAccCheckComputeV2InstanceFloatingIPAttach( } } -var testAccComputeV2Instance_basic = fmt.Sprintf(` - resource "openstack_compute_instance_v2" "foo" { - region = "%s" - name = "terraform-test" - metadata { - foo = "bar" - } - }`, - OS_REGION_NAME) - var testAccComputeV2Instance_volumeAttach = fmt.Sprintf(` resource "openstack_blockstorage_volume_v1" "myvol" { name = "myvol" From 478379b3b32414737a5bc8cca34f433d9b451a61 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Apr 2015 22:49:05 -0700 Subject: [PATCH 42/59] providers/terraform: name it terraform_remote_state --- builtin/providers/terraform/provider.go | 2 +- builtin/providers/terraform/resource_state.go | 23 +++++++++++-------- .../terraform/resource_state_test.go | 4 ++-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/builtin/providers/terraform/provider.go b/builtin/providers/terraform/provider.go index 0330ce775307..e71d5f40a39b 100644 --- a/builtin/providers/terraform/provider.go +++ b/builtin/providers/terraform/provider.go @@ -9,7 +9,7 @@ import ( func Provider() terraform.ResourceProvider { return &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ - "terraform_state": resourceState(), + "terraform_remote_state": resourceRemoteState(), }, } } diff --git a/builtin/providers/terraform/resource_state.go b/builtin/providers/terraform/resource_state.go index 85de3990d65d..fb0e85ee2c7e 100644 --- a/builtin/providers/terraform/resource_state.go +++ b/builtin/providers/terraform/resource_state.go @@ -8,11 +8,11 @@ import ( "github.com/hashicorp/terraform/state/remote" ) -func resourceState() *schema.Resource { +func resourceRemoteState() *schema.Resource { return &schema.Resource{ - Create: resourceStateCreate, - Read: resourceStateRead, - Delete: resourceStateDelete, + Create: resourceRemoteStateCreate, + Read: resourceRemoteStateRead, + Delete: resourceRemoteStateDelete, Schema: map[string]*schema.Schema{ "backend": &schema.Schema{ @@ -35,11 +35,11 @@ func resourceState() *schema.Resource { } } -func resourceStateCreate(d *schema.ResourceData, meta interface{}) error { - return resourceStateRead(d, meta) +func resourceRemoteStateCreate(d *schema.ResourceData, meta interface{}) error { + return resourceRemoteStateRead(d, meta) } -func resourceStateRead(d *schema.ResourceData, meta interface{}) error { +func resourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error { backend := d.Get("backend").(string) config := make(map[string]string) for k, v := range d.Get("config").(map[string]interface{}) { @@ -60,12 +60,17 @@ func resourceStateRead(d *schema.ResourceData, meta interface{}) error { return err } + var outputs map[string]string + if !state.State().Empty() { + outputs = state.State().RootModule().Outputs + } + d.SetId(time.Now().UTC().String()) - d.Set("output", state.State().RootModule().Outputs) + d.Set("output", outputs) return nil } -func resourceStateDelete(d *schema.ResourceData, meta interface{}) error { +func resourceRemoteStateDelete(d *schema.ResourceData, meta interface{}) error { d.SetId("") return nil } diff --git a/builtin/providers/terraform/resource_state_test.go b/builtin/providers/terraform/resource_state_test.go index 6db173503945..42ad55adac98 100644 --- a/builtin/providers/terraform/resource_state_test.go +++ b/builtin/providers/terraform/resource_state_test.go @@ -17,7 +17,7 @@ func TestAccState_basic(t *testing.T) { Config: testAccState_basic, Check: resource.ComposeTestCheckFunc( testAccCheckStateValue( - "terraform_state.foo", "foo", "bar"), + "terraform_remote_state.foo", "foo", "bar"), ), }, }, @@ -45,7 +45,7 @@ func testAccCheckStateValue(id, name, value string) resource.TestCheckFunc { } const testAccState_basic = ` -resource "terraform_state" "foo" { +resource "terraform_remote_state" "foo" { backend = "_local" config { From 2721f6235e66e2f0ae88b46dd25bceabcf87528b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Apr 2015 22:52:13 -0700 Subject: [PATCH 43/59] update cHANGELOG --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19332c01d5fc..558c87d0de0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ FEATURES: using the standard Docker API. [GH-855] * **New provider: `openstack` (OpenStack)** - Interact with the many resources provided by OpenStack. [GH-924] + * **New feature: `terraform_remote_state` resource** - Reference remote + states from other Terraform runs to use Terraform outputs as inputs + into another Terraform run. * **New command: `taint`** - Manually mark a resource as tainted, causing a destroy and recreate on the next plan/apply. * **New resource: `aws_vpn_gateway`** [GH-1137] @@ -57,8 +60,8 @@ IMPROVEMENTS: `www` instead of `www.example.com`. * providers/aws: Improve dependency violation error handling, when deleting Internet Gateways or Auto Scaling groups [GH-1325]. - * provider/aws: Add non-destructive updates to AWS RDS. You can now upgrade - `egine_version`, `parameter_group_name`, and `multi_az` without forcing + * provider/aws: Add non-destructive updates to AWS RDS. You can now upgrade + `egine_version`, `parameter_group_name`, and `multi_az` without forcing a new database to be created.[GH-1341] BUG FIXES: From 31531e5414b4306aa6083f40a76d4f6d3e67227c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Apr 2015 22:55:16 -0700 Subject: [PATCH 44/59] update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 558c87d0de0f..c920e5fc413c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,10 +59,11 @@ IMPROVEMENTS: * providers/aws: Add a short syntax for Route 53 Record names, e.g. `www` instead of `www.example.com`. * providers/aws: Improve dependency violation error handling, when deleting - Internet Gateways or Auto Scaling groups [GH-1325]. + Internet Gateways or Auto Scaling groups [GH-1325]. * provider/aws: Add non-destructive updates to AWS RDS. You can now upgrade `egine_version`, `parameter_group_name`, and `multi_az` without forcing a new database to be created.[GH-1341] + * provisioners/remote-exec: SSH agent support. [GH-1208] BUG FIXES: From 87e1260fac3ee68c2661f36888bba6ad412114a8 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Thu, 2 Apr 2015 09:00:47 -0500 Subject: [PATCH 45/59] update hash for aws security group test --- builtin/providers/aws/resource_aws_security_group_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/builtin/providers/aws/resource_aws_security_group_test.go b/builtin/providers/aws/resource_aws_security_group_test.go index 067cda8a1c1e..63d04f813110 100644 --- a/builtin/providers/aws/resource_aws_security_group_test.go +++ b/builtin/providers/aws/resource_aws_security_group_test.go @@ -76,13 +76,13 @@ func TestAccAWSSecurityGroup_self(t *testing.T) { resource.TestCheckResourceAttr( "aws_security_group.web", "description", "Used in the terraform acceptance tests"), resource.TestCheckResourceAttr( - "aws_security_group.web", "ingress.3128515109.protocol", "tcp"), + "aws_security_group.web", "ingress.3971148406.protocol", "tcp"), resource.TestCheckResourceAttr( - "aws_security_group.web", "ingress.3128515109.from_port", "80"), + "aws_security_group.web", "ingress.3971148406.from_port", "80"), resource.TestCheckResourceAttr( - "aws_security_group.web", "ingress.3128515109.to_port", "8000"), + "aws_security_group.web", "ingress.3971148406.to_port", "8000"), resource.TestCheckResourceAttr( - "aws_security_group.web", "ingress.3128515109.self", "true"), + "aws_security_group.web", "ingress.3971148406.self", "true"), checkSelf, ), }, From ace47c1c5b805fefab077ee475ecee1a64c1246b Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Thu, 2 Apr 2015 09:04:59 -0500 Subject: [PATCH 46/59] providers/digitalocean: fix ssh key test there's now validation on the public key field --- .../resource_digitalocean_ssh_key_test.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/builtin/providers/digitalocean/resource_digitalocean_ssh_key_test.go b/builtin/providers/digitalocean/resource_digitalocean_ssh_key_test.go index d5c50e6f8d3c..009366e18a0f 100644 --- a/builtin/providers/digitalocean/resource_digitalocean_ssh_key_test.go +++ b/builtin/providers/digitalocean/resource_digitalocean_ssh_key_test.go @@ -2,6 +2,8 @@ package digitalocean import ( "fmt" + "strconv" + "strings" "testing" "github.com/hashicorp/terraform/helper/resource" @@ -25,7 +27,7 @@ func TestAccDigitalOceanSSHKey_Basic(t *testing.T) { resource.TestCheckResourceAttr( "digitalocean_ssh_key.foobar", "name", "foobar"), resource.TestCheckResourceAttr( - "digitalocean_ssh_key.foobar", "public_key", "abcdef"), + "digitalocean_ssh_key.foobar", "public_key", testAccValidPublicKey), ), }, }, @@ -82,7 +84,7 @@ func testAccCheckDigitalOceanSSHKeyExists(n string, key *digitalocean.SSHKey) re return err } - if foundKey.Name != rs.Primary.ID { + if strconv.Itoa(int(foundKey.Id)) != rs.Primary.ID { return fmt.Errorf("Record not found") } @@ -92,8 +94,12 @@ func testAccCheckDigitalOceanSSHKeyExists(n string, key *digitalocean.SSHKey) re } } -const testAccCheckDigitalOceanSSHKeyConfig_basic = ` +var testAccCheckDigitalOceanSSHKeyConfig_basic = fmt.Sprintf(` resource "digitalocean_ssh_key" "foobar" { name = "foobar" - public_key = "abcdef" -}` + public_key = "%s" +}`, testAccValidPublicKey) + +var testAccValidPublicKey = strings.TrimSpace(` +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCKVmnMOlHKcZK8tpt3MP1lqOLAcqcJzhsvJcjscgVERRN7/9484SOBJ3HSKxxNG5JN8owAjy5f9yYwcUg+JaUVuytn5Pv3aeYROHGGg+5G346xaq3DAwX6Y5ykr2fvjObgncQBnuU5KHWCECO/4h8uWuwh/kfniXPVjFToc+gnkqA+3RKpAecZhFXwfalQ9mMuYGFxn+fwn8cYEApsJbsEmb0iJwPiZ5hjFC8wREuiTlhPHDgkBLOiycd20op2nXzDbHfCHInquEe/gYxEitALONxm0swBOwJZwlTDOB7C6y2dzlrtxr1L59m7pCkWI4EtTRLvleehBoj3u7jB4usR +`) From 3fcaab91143babed1f4c19991cea8b4d994fb04b Mon Sep 17 00:00:00 2001 From: Hart Hoover Date: Thu, 2 Apr 2015 10:43:40 -0500 Subject: [PATCH 47/59] Fix(docs) Correct spelling error in Docker documentation --- website/source/docs/providers/docker/index.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/providers/docker/index.html.markdown b/website/source/docs/providers/docker/index.html.markdown index 9a1f4abe1ab1..9d057fd34654 100644 --- a/website/source/docs/providers/docker/index.html.markdown +++ b/website/source/docs/providers/docker/index.html.markdown @@ -10,7 +10,7 @@ description: |- The Docker provider is used to interact with Docker containers and images. It uses the Docker API to manage the lifecycle of Docker containers. Because -the Docker provider uses the Docker API, it is immediatel compatible not +the Docker provider uses the Docker API, it is immediately compatible not only with single server Docker but Swarm and any additional Docker-compatible API hosts. From c6a884972536772d1545dad6209ee0efba63c9eb Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Thu, 2 Apr 2015 16:12:18 +0000 Subject: [PATCH 48/59] OpenStack Documentation Updates --- .../providers/openstack/index.html.markdown | 28 ++++++++++-- .../r/blockstorage_volume_v1.html.markdown | 3 ++ .../r/compute_instance_v2.html.markdown | 45 +++++++++++++++---- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/website/source/docs/providers/openstack/index.html.markdown b/website/source/docs/providers/openstack/index.html.markdown index da6d1fb79dbe..02b8c8dc8a4a 100644 --- a/website/source/docs/providers/openstack/index.html.markdown +++ b/website/source/docs/providers/openstack/index.html.markdown @@ -35,13 +35,16 @@ resource "openstack_compute_instance_v2" "test-server" { The following arguments are supported: -* `auth_url` - (Required) +* `auth_url` - (Required) If omitted, the `OS_AUTH_URL` environment + variable is used. -* `user_name` - (Optional; Required for Identity V2) +* `user_name` - (Optional; Required for Identity V2) If omitted, the + `OS_USERNAME` environment variable is used. * `user_id` - (Optional) -* `password` - (Optional; Required if not using `api_key`) +* `password` - (Optional; Required if not using `api_key`) If omitted, the + `OS_PASSWORD` environment variable is used. * `api_key` - (Optional; Required if not using `password`) @@ -51,4 +54,21 @@ The following arguments are supported: * `tenant_id` - (Optional) -* `tenant_name` - (Optional) +* `tenant_name` - (Optional) If omitted, the `OS_TENANT_NAME` environment + variable is used. + +## Testing + +In order to run the Acceptance Tests for development, the following environment +variables must also be set: + +* `OS_REGION_NAME` - The region in which to create the server instance. + +* `OS_IMAGE_ID` or `OS_IMAGE_NAME` - a UUID or name of an existing image in + Glance. + +* `OS_FLAVOR_ID` or `OS_FLAVOR_NAME` - an ID or name of an existing flavor. + +* `OS_POOL_NAME` - The name of a Floating IP pool. + +* `OS_NETWORK_ID` - The UUID of a network in your test environment. diff --git a/website/source/docs/providers/openstack/r/blockstorage_volume_v1.html.markdown b/website/source/docs/providers/openstack/r/blockstorage_volume_v1.html.markdown index 0b91eb11c6bd..c547994cca4d 100644 --- a/website/source/docs/providers/openstack/r/blockstorage_volume_v1.html.markdown +++ b/website/source/docs/providers/openstack/r/blockstorage_volume_v1.html.markdown @@ -66,3 +66,6 @@ The following attributes are exported: * `snapshot_id` - See Argument Reference above. * `metadata` - See Argument Reference above. * `volume_type` - See Argument Reference above. +* `attachment` - If a volume is attached to an instance, this attribute will + display the Attachment ID, Instance ID, and the Device as the Instance + sees it. diff --git a/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown b/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown index 36805ed0d28c..7d5874457cb4 100644 --- a/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown +++ b/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown @@ -47,6 +47,12 @@ The following arguments are supported: * `flavor_name` - (Optional; Required if `flavor_id` is empty) The name of the desired flavor for the server. Changing this resizes the existing server. +* `floating_ip` - (Optional) A Floating IP that will be associated with the + Instance. The Floating IP must be provisioned already. + +* `user_data` - (Optional) The user data to provide when launching the instance. + Changing this creates a new server. + * `security_groups` - (Optional) An array of one or more security group names to associate with the server. Changing this results in adding/removing security groups from the existing server. @@ -61,6 +67,9 @@ The following arguments are supported: * `metadata` - (Optional) Metadata key/value pairs to make available from within the instance. Changing this updates the existing server metadata. +* `config_drive` - (Optional) Whether to use the config_drive feature to + configure the instance. Changing this creates a new server. + * `admin_pass` - (Optional) The administrative password to assign to the server. Changing this changes the root password on the existing server. @@ -76,13 +85,16 @@ The following arguments are supported: The `network` block supports: -* `uuid` - (Required unless `port` is provided) The network UUID to attach to - the server. - -* `port` - (Required unless `uuid` is provided) The port UUID of a network to +* `uuid` - (Required unless `port` or `name` is provided) The network UUID to attach to the server. -* `fixed_ip` - (Optional) Specifies a fixed IP address to be used on this +* `name` - (Required unless `uuid` or `port` is provided) The human-readable + name of the network. + +* `port` - (Required unless `uuid` or `name` is provided) The port UUID of a + network to attach to the server. + +* `fixed_ip_v4` - (Optional) Specifies a fixed IPv4 address to be used on this network. The `block_device` block supports: @@ -113,8 +125,25 @@ The following attributes are exported: * `region` - See Argument Reference above. * `name` - See Argument Reference above. -* `access_ip_v4` - See Argument Reference above. -* `access_ip_v6` - See Argument Reference above. +* `access_ip_v4` - The first detected Fixed IPv4 address _or_ the + Floating IP. +* `access_ip_v6` - The first detected Fixed IPv6 address. * `metadata` - See Argument Reference above. * `security_groups` - See Argument Reference above. -* `flavor_ref` - See Argument Reference above. +* `flavor_id` - See Argument Reference above. +* `flavor_name` - See Argument Reference above. +* `network/uuid` - See Argument Reference above. +* `network/name` - See Argument Reference above. +* `network/port` - See Argument Reference above. +* `network/fixed_ip_v4` - The Fixed IPv4 address of the Instance on that + network. +* `network/fixed_ip_v6` - The Fixed IPv6 address of the Instance on that + network. +* `network/mac` - The MAC address of the NIC on that network. + +## Notes + +If you configure the instance to have multiple networks, be aware that only +the first network can be associated with a Floating IP. So the first network +in the instance resource _must_ be the network that you have configured to +communicate with your floating IP / public network via a Neutron Router. From f77250f17d48074c215df1412d0d02df2d99593d Mon Sep 17 00:00:00 2001 From: Jason Waldrip Date: Thu, 2 Apr 2015 09:47:37 -0600 Subject: [PATCH 49/59] block device support for launch configurations - mimics block device support from AWS instance - splits the acceptance tests out so they all pass, handling a FIXME from #1079 --- .../aws/resource_aws_launch_configuration.go | 338 +++++++++++++++++- .../resource_aws_launch_configuration_test.go | 124 +++++-- .../aws/r/launch_config.html.markdown | 51 +++ 3 files changed, 483 insertions(+), 30 deletions(-) diff --git a/builtin/providers/aws/resource_aws_launch_configuration.go b/builtin/providers/aws/resource_aws_launch_configuration.go index 84edd9fd4f3b..854c0feb9157 100644 --- a/builtin/providers/aws/resource_aws_launch_configuration.go +++ b/builtin/providers/aws/resource_aws_launch_configuration.go @@ -1,6 +1,7 @@ package aws import ( + "bytes" "crypto/sha1" "encoding/base64" "encoding/hex" @@ -10,6 +11,7 @@ import ( "github.com/hashicorp/aws-sdk-go/aws" "github.com/hashicorp/aws-sdk-go/gen/autoscaling" + "github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" @@ -90,27 +92,197 @@ func resourceAwsLaunchConfiguration() *schema.Resource { Optional: true, ForceNew: true, }, + + "ebs_optimized": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "placement_tenancy": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "ebs_block_device": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delete_on_termination": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + ForceNew: true, + }, + + "device_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "iops": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "snapshot_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "volume_size": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "volume_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool))) + buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) + // NOTE: Not considering IOPS in hash; when using gp2, IOPS can come + // back set to something like "33", which throws off the set + // calculation and generates an unresolvable diff. + // buf.WriteString(fmt.Sprintf("%d-", m["iops"].(int))) + buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string))) + buf.WriteString(fmt.Sprintf("%d-", m["volume_size"].(int))) + buf.WriteString(fmt.Sprintf("%s-", m["volume_type"].(string))) + return hashcode.String(buf.String()) + }, + }, + + "ephemeral_block_device": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "device_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "virtual_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) + return hashcode.String(buf.String()) + }, + }, + + "root_block_device": &schema.Schema{ + // TODO: This is a set because we don't support singleton + // sub-resources today. We'll enforce that the set only ever has + // length zero or one below. When TF gains support for + // sub-resources this can be converted. + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + // "You can only modify the volume size, volume type, and Delete on + // Termination flag on the block device mapping entry for the root + // device volume." - bit.ly/ec2bdmap + Schema: map[string]*schema.Schema{ + "delete_on_termination": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + ForceNew: true, + }, + + "iops": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "volume_size": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "volume_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool))) + // See the NOTE in "ebs_block_device" for why we skip iops here. + // buf.WriteString(fmt.Sprintf("%d-", m["iops"].(int))) + buf.WriteString(fmt.Sprintf("%d-", m["volume_size"].(int))) + buf.WriteString(fmt.Sprintf("%s-", m["volume_type"].(string))) + return hashcode.String(buf.String()) + }, + }, }, } } func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface{}) error { autoscalingconn := meta.(*AWSClient).autoscalingconn + ec2conn := meta.(*AWSClient).ec2conn - var createLaunchConfigurationOpts autoscaling.CreateLaunchConfigurationType - createLaunchConfigurationOpts.LaunchConfigurationName = aws.String(d.Get("name").(string)) - createLaunchConfigurationOpts.ImageID = aws.String(d.Get("image_id").(string)) - createLaunchConfigurationOpts.InstanceType = aws.String(d.Get("instance_type").(string)) + createLaunchConfigurationOpts := autoscaling.CreateLaunchConfigurationType{ + LaunchConfigurationName: aws.String(d.Get("name").(string)), + ImageID: aws.String(d.Get("image_id").(string)), + InstanceType: aws.String(d.Get("instance_type").(string)), + EBSOptimized: aws.Boolean(d.Get("ebs_optimized").(bool)), + } if v, ok := d.GetOk("user_data"); ok { - createLaunchConfigurationOpts.UserData = aws.String(base64.StdEncoding.EncodeToString([]byte(v.(string)))) - } - if v, ok := d.GetOk("associate_public_ip_address"); ok { - createLaunchConfigurationOpts.AssociatePublicIPAddress = aws.Boolean(v.(bool)) + userData := base64.StdEncoding.EncodeToString([]byte(v.(string))) + createLaunchConfigurationOpts.UserData = aws.String(userData) } + if v, ok := d.GetOk("iam_instance_profile"); ok { createLaunchConfigurationOpts.IAMInstanceProfile = aws.String(v.(string)) } + + if v, ok := d.GetOk("placement_tenancy"); ok { + createLaunchConfigurationOpts.PlacementTenancy = aws.String(v.(string)) + } + + if v := d.Get("associate_public_ip_address"); v != nil { + createLaunchConfigurationOpts.AssociatePublicIPAddress = aws.Boolean(v.(bool)) + } else { + createLaunchConfigurationOpts.AssociatePublicIPAddress = aws.Boolean(false) + } + if v, ok := d.GetOk("key_name"); ok { createLaunchConfigurationOpts.KeyName = aws.String(v.(string)) } @@ -120,7 +292,90 @@ func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface if v, ok := d.GetOk("security_groups"); ok { createLaunchConfigurationOpts.SecurityGroups = expandStringList( - v.(*schema.Set).List()) + v.(*schema.Set).List(), + ) + } + + var blockDevices []autoscaling.BlockDeviceMapping + + if v, ok := d.GetOk("ebs_block_device"); ok { + vL := v.(*schema.Set).List() + for _, v := range vL { + bd := v.(map[string]interface{}) + ebs := &autoscaling.EBS{ + DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)), + } + + if v, ok := bd["snapshot_id"].(string); ok && v != "" { + ebs.SnapshotID = aws.String(v) + } + + if v, ok := bd["volume_size"].(int); ok && v != 0 { + ebs.VolumeSize = aws.Integer(v) + } + + if v, ok := bd["volume_type"].(string); ok && v != "" { + ebs.VolumeType = aws.String(v) + } + + if v, ok := bd["iops"].(int); ok && v > 0 { + ebs.IOPS = aws.Integer(v) + } + + blockDevices = append(blockDevices, autoscaling.BlockDeviceMapping{ + DeviceName: aws.String(bd["device_name"].(string)), + EBS: ebs, + }) + } + } + + if v, ok := d.GetOk("ephemeral_block_device"); ok { + vL := v.(*schema.Set).List() + for _, v := range vL { + bd := v.(map[string]interface{}) + blockDevices = append(blockDevices, autoscaling.BlockDeviceMapping{ + DeviceName: aws.String(bd["device_name"].(string)), + VirtualName: aws.String(bd["virtual_name"].(string)), + }) + } + } + + if v, ok := d.GetOk("root_block_device"); ok { + vL := v.(*schema.Set).List() + if len(vL) > 1 { + return fmt.Errorf("Cannot specify more than one root_block_device.") + } + for _, v := range vL { + bd := v.(map[string]interface{}) + ebs := &autoscaling.EBS{ + DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)), + } + + if v, ok := bd["volume_size"].(int); ok && v != 0 { + ebs.VolumeSize = aws.Integer(v) + } + + if v, ok := bd["volume_type"].(string); ok && v != "" { + ebs.VolumeType = aws.String(v) + } + + if v, ok := bd["iops"].(int); ok && v > 0 { + ebs.IOPS = aws.Integer(v) + } + + if dn, err := fetchRootDeviceName(d.Get("image_id").(string), ec2conn); err == nil { + blockDevices = append(blockDevices, autoscaling.BlockDeviceMapping{ + DeviceName: dn, + EBS: ebs, + }) + } else { + return err + } + } + } + + if len(blockDevices) > 0 { + createLaunchConfigurationOpts.BlockDeviceMappings = blockDevices } if v, ok := d.GetOk("name"); ok { @@ -151,6 +406,7 @@ func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface func resourceAwsLaunchConfigurationRead(d *schema.ResourceData, meta interface{}) error { autoscalingconn := meta.(*AWSClient).autoscalingconn + ec2conn := meta.(*AWSClient).ec2conn describeOpts := autoscaling.LaunchConfigurationNamesType{ LaunchConfigurationNames: []string{d.Id()}, @@ -197,6 +453,11 @@ func resourceAwsLaunchConfigurationRead(d *schema.ResourceData, meta interface{} } else { d.Set("security_groups", nil) } + + if err := readLCBlockDevices(d, &lc, ec2conn); err != nil { + return err + } + return nil } @@ -217,3 +478,62 @@ func resourceAwsLaunchConfigurationDelete(d *schema.ResourceData, meta interface return nil } + +func readLCBlockDevices(d *schema.ResourceData, lc *autoscaling.LaunchConfiguration, ec2conn *ec2.EC2) error { + ibds, err := readBlockDevicesFromLaunchConfiguration(d, lc, ec2conn) + if err != nil { + return err + } + + if err := d.Set("ebs_block_device", ibds["ebs"]); err != nil { + return err + } + if ibds["root"] != nil { + if err := d.Set("root_block_device", []interface{}{ibds["root"]}); err != nil { + return err + } + } + + return nil +} + +func readBlockDevicesFromLaunchConfiguration(d *schema.ResourceData, lc *autoscaling.LaunchConfiguration, ec2conn *ec2.EC2) ( + map[string]interface{}, error) { + blockDevices := make(map[string]interface{}) + blockDevices["ebs"] = make([]map[string]interface{}, 0) + blockDevices["root"] = nil + if len(lc.BlockDeviceMappings) == 0 { + return nil, nil + } + rootDeviceName, err := fetchRootDeviceName(d.Get("image_id").(string), ec2conn) + if err == nil { + return nil, err + } + for _, bdm := range lc.BlockDeviceMappings { + bd := make(map[string]interface{}) + if bdm.EBS != nil && bdm.EBS.DeleteOnTermination != nil { + bd["delete_on_termination"] = *bdm.EBS.DeleteOnTermination + } + if bdm.EBS != nil && bdm.EBS.VolumeSize != nil { + bd["volume_size"] = bdm.EBS.VolumeSize + } + if bdm.EBS != nil && bdm.EBS.VolumeType != nil { + bd["volume_type"] = *bdm.EBS.VolumeType + } + if bdm.EBS != nil && bdm.EBS.IOPS != nil { + bd["iops"] = *bdm.EBS.IOPS + } + if bdm.DeviceName != nil && bdm.DeviceName == rootDeviceName { + blockDevices["root"] = bd + } else { + if bdm.DeviceName != nil { + bd["device_name"] = *bdm.DeviceName + } + if bdm.EBS != nil && bdm.EBS.SnapshotID != nil { + bd["snapshot_id"] = *bdm.EBS.SnapshotID + } + blockDevices["ebs"] = append(blockDevices["ebs"].([]map[string]interface{}), bd) + } + } + return blockDevices, nil +} diff --git a/builtin/providers/aws/resource_aws_launch_configuration_test.go b/builtin/providers/aws/resource_aws_launch_configuration_test.go index eb557f08e61f..f300ad258de3 100644 --- a/builtin/providers/aws/resource_aws_launch_configuration_test.go +++ b/builtin/providers/aws/resource_aws_launch_configuration_test.go @@ -2,7 +2,10 @@ package aws import ( "fmt" + "math/rand" + "strings" "testing" + "time" "github.com/hashicorp/aws-sdk-go/aws" "github.com/hashicorp/aws-sdk-go/gen/autoscaling" @@ -10,7 +13,7 @@ import ( "github.com/hashicorp/terraform/terraform" ) -func TestAccAWSLaunchConfiguration(t *testing.T) { +func TestAccAWSLaunchConfiguration_withBlockDevices(t *testing.T) { var conf autoscaling.LaunchConfiguration resource.Test(t, resource.TestCase{ @@ -26,39 +29,75 @@ func TestAccAWSLaunchConfiguration(t *testing.T) { resource.TestCheckResourceAttr( "aws_launch_configuration.bar", "image_id", "ami-21f78e11"), resource.TestCheckResourceAttr( - "aws_launch_configuration.bar", "name", "foobar-terraform-test"), - resource.TestCheckResourceAttr( - "aws_launch_configuration.bar", "instance_type", "t1.micro"), + "aws_launch_configuration.bar", "instance_type", "m1.small"), resource.TestCheckResourceAttr( "aws_launch_configuration.bar", "associate_public_ip_address", "true"), resource.TestCheckResourceAttr( "aws_launch_configuration.bar", "spot_price", ""), ), }, + }, + }) +} +func TestAccAWSLaunchConfiguration_withSpotPrice(t *testing.T) { + var conf autoscaling.LaunchConfiguration + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLaunchConfigurationDestroy, + Steps: []resource.TestStep{ resource.TestStep{ - Config: TestAccAWSLaunchConfigurationWithSpotPriceConfig, + Config: testAccAWSLaunchConfigurationWithSpotPriceConfig, Check: resource.ComposeTestCheckFunc( testAccCheckAWSLaunchConfigurationExists("aws_launch_configuration.bar", &conf), - testAccCheckAWSLaunchConfigurationAttributes(&conf), resource.TestCheckResourceAttr( "aws_launch_configuration.bar", "spot_price", "0.01"), ), }, + }, + }) +} + +func TestAccAWSLaunchConfiguration_withGeneratedName(t *testing.T) { + var conf autoscaling.LaunchConfiguration + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLaunchConfigurationDestroy, + Steps: []resource.TestStep{ resource.TestStep{ Config: testAccAWSLaunchConfigurationNoNameConfig, Check: resource.ComposeTestCheckFunc( testAccCheckAWSLaunchConfigurationExists("aws_launch_configuration.bar", &conf), - testAccCheckAWSLaunchConfigurationAttributes(&conf), - resource.TestCheckResourceAttr( - "aws_launch_configuration.bar", "name", "terraform-foo"), // FIXME - This should fail?!?!? + testAccCheckAWSLaunchConfigurationGeneratedNamePrefix( + "aws_launch_configuration.bar", "terraform-"), ), }, }, }) } +func testAccCheckAWSLaunchConfigurationGeneratedNamePrefix( + resource, prefix string) resource.TestCheckFunc { + return func(s *terraform.State) error { + r, ok := s.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("Resource not found") + } + name, ok := r.Primary.Attributes["name"] + if !ok { + return fmt.Errorf("Name attr not found: %#v", r.Primary.Attributes) + } + if !strings.HasPrefix(name, prefix) { + return fmt.Errorf("Name: %q, does not have prefix: %q", name, prefix) + } + return nil + } +} + func testAccCheckAWSLaunchConfigurationDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).autoscalingconn @@ -98,14 +137,40 @@ func testAccCheckAWSLaunchConfigurationAttributes(conf *autoscaling.LaunchConfig return fmt.Errorf("Bad image_id: %s", *conf.ImageID) } - if *conf.LaunchConfigurationName != "foobar-terraform-test" { + if !strings.HasPrefix(*conf.LaunchConfigurationName, "terraform-") { return fmt.Errorf("Bad name: %s", *conf.LaunchConfigurationName) } - if *conf.InstanceType != "t1.micro" { + if *conf.InstanceType != "m1.small" { return fmt.Errorf("Bad instance_type: %s", *conf.InstanceType) } + // Map out the block devices by name, which should be unique. + blockDevices := make(map[string]autoscaling.BlockDeviceMapping) + for _, blockDevice := range conf.BlockDeviceMappings { + blockDevices[*blockDevice.DeviceName] = blockDevice + } + + // Check if the root block device exists. + if _, ok := blockDevices["/dev/sda1"]; !ok { + fmt.Errorf("block device doesn't exist: /dev/sda1") + } + + // Check if the secondary block device exists. + if _, ok := blockDevices["/dev/sdb"]; !ok { + fmt.Errorf("block device doesn't exist: /dev/sdb") + } + + // Check if the third block device exists. + if _, ok := blockDevices["/dev/sdc"]; !ok { + fmt.Errorf("block device doesn't exist: /dev/sdc") + } + + // Check if the secondary block device exists. + if _, ok := blockDevices["/dev/sdb"]; !ok { + return fmt.Errorf("block device doesn't exist: /dev/sdb") + } + return nil } } @@ -143,30 +208,47 @@ func testAccCheckAWSLaunchConfigurationExists(n string, res *autoscaling.LaunchC } } -const testAccAWSLaunchConfigurationConfig = ` +var testAccAWSLaunchConfigurationConfig = fmt.Sprintf(` resource "aws_launch_configuration" "bar" { - name = "foobar-terraform-test" + name = "terraform-test-%d" image_id = "ami-21f78e11" - instance_type = "t1.micro" + instance_type = "m1.small" user_data = "foobar-user-data" associate_public_ip_address = true + + root_block_device { + volume_type = "gp2" + volume_size = 11 + } + ebs_block_device { + device_name = "/dev/sdb" + volume_size = 9 + } + ebs_block_device { + device_name = "/dev/sdc" + volume_size = 10 + volume_type = "io1" + iops = 100 + } + ephemeral_block_device { + device_name = "/dev/sde" + virtual_name = "ephemeral0" + } } -` +`, rand.New(rand.NewSource(time.Now().UnixNano())).Int()) -const TestAccAWSLaunchConfigurationWithSpotPriceConfig = ` +var testAccAWSLaunchConfigurationWithSpotPriceConfig = fmt.Sprintf(` resource "aws_launch_configuration" "bar" { - name = "foobar-terraform-test" + name = "terraform-test-%d" image_id = "ami-21f78e11" instance_type = "t1.micro" - user_data = "foobar-user-data" - associate_public_ip_address = true spot_price = "0.01" } -` +`, rand.New(rand.NewSource(time.Now().UnixNano())).Int()) const testAccAWSLaunchConfigurationNoNameConfig = ` resource "aws_launch_configuration" "bar" { - image_id = "ami-21f78e12" + image_id = "ami-21f78e11" instance_type = "t1.micro" user_data = "foobar-user-data-change" associate_public_ip_address = false diff --git a/website/source/docs/providers/aws/r/launch_config.html.markdown b/website/source/docs/providers/aws/r/launch_config.html.markdown index 677f3b088084..5a80a97b8dd6 100644 --- a/website/source/docs/providers/aws/r/launch_config.html.markdown +++ b/website/source/docs/providers/aws/r/launch_config.html.markdown @@ -33,6 +33,57 @@ The following arguments are supported: * `security_groups` - (Optional) A list of associated security group IDS. * `associate_public_ip_address` - (Optional) Associate a public ip address with an instance in a VPC. * `user_data` - (Optional) The user data to provide when launching the instance. +* `block_device_mapping` - (Optional) A list of block devices to add. Their keys are documented below. + + +## Block devices + +Each of the `*_block_device` attributes controls a portion of the AWS +Launch Configuration's "Block Device Mapping". It's a good idea to familiarize yourself with [AWS's Block Device +Mapping docs](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html) +to understand the implications of using these attributes. + +The `root_block_device` mapping supports the following: + +* `volume_type` - (Optional) The type of volume. Can be `"standard"`, `"gp2"`, + or `"io1"`. (Default: `"standard"`). +* `volume_size` - (Optional) The size of the volume in gigabytes. +* `iops` - (Optional) The amount of provisioned + [IOPS](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-io-characteristics.html). + This must be set with a `volume_type` of `"io1"`. +* `delete_on_termination` - (Optional) Whether the volume should be destroyed + on instance termination (Default: `true`). + +Modifying any of the `root_block_device` settings requires resource +replacement. + +Each `ebs_block_device` supports the following: + +* `device_name` - The name of the device to mount. +* `snapshot_id` - (Optional) The Snapshot ID to mount. +* `volume_type` - (Optional) The type of volume. Can be `"standard"`, `"gp2"`, + or `"io1"`. (Default: `"standard"`). +* `volume_size` - (Optional) The size of the volume in gigabytes. +* `iops` - (Optional) The amount of provisioned + [IOPS](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-io-characteristics.html). + This must be set with a `volume_type` of `"io1"`. +* `delete_on_termination` - (Optional) Whether the volume should be destroyed + on instance termination (Default: `true`). + +Modifying any `ebs_block_device` currently requires resource replacement. + +Each `ephemeral_block_device` supports the following: + +* `device_name` - The name of the block device to mount on the instance. +* `virtual_name` - The [Instance Store Device + Name](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html#InstanceStoreDeviceNames) + (e.g. `"ephemeral0"`) + +Each AWS Instance type has a different set of Instance Store block devices +available for attachment. AWS [publishes a +list](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html#StorageOnInstanceTypes) +of which ephemeral devices are available on each type. The devices are always +identified by the `virtual_name` in the format `"ephemeral{0..N}"`. ## Attributes Reference From 9fd14f16382b05c41ebc719e966c1386313b7da8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Apr 2015 10:42:10 -0700 Subject: [PATCH 50/59] v0.4.0 --- CHANGELOG.md | 2 +- version.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c920e5fc413c..dac6f49a6c5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.4.0 (unreleased) +## 0.4.0 (April 2, 2015) BACKWARDS INCOMPATIBILITIES: diff --git a/version.go b/version.go index 8862f9e35a76..bcabe0a71f85 100644 --- a/version.go +++ b/version.go @@ -9,4 +9,4 @@ const Version = "0.4.0" // A pre-release marker for the version. If this is "" (empty string) // then it means that it is a final release. Otherwise, this is a pre-release // such as "dev" (in development), "beta", "rc1", etc. -const VersionPrerelease = "dev" +const VersionPrerelease = "" From a2cd804e173a48eade5885d031ec37ef00bc6506 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Thu, 2 Apr 2015 12:51:01 -0500 Subject: [PATCH 51/59] record deps for 0.4.0 release --- deps/v0-4-0.json | 81 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 deps/v0-4-0.json diff --git a/deps/v0-4-0.json b/deps/v0-4-0.json new file mode 100644 index 000000000000..1dcbc7356e44 --- /dev/null +++ b/deps/v0-4-0.json @@ -0,0 +1,81 @@ +{ + "ImportPath": "github.com/hashicorp/terraform", + "GoVersion": "go1.4.2", + "Deps": [ + { + "ImportPath": "github.com/hashicorp/atlas-go/archive", + "Comment": "20141209094003-55-g8663626", + "Rev": "86636264d03bc142dcd136d02811c469ba542444" + }, + { + "ImportPath": "github.com/hashicorp/atlas-go/v1", + "Comment": "20141209094003-55-g8663626", + "Rev": "86636264d03bc142dcd136d02811c469ba542444" + }, + { + "ImportPath": "github.com/hashicorp/consul/api", + "Comment": "v0.5.0-127-g8724845", + "Rev": "872484596472df47b95128f5996776fd73eda26c" + }, + { + "ImportPath": "github.com/hashicorp/errwrap", + "Rev": "7554cd9344cec97297fa6649b055a8c98c2a1e55" + }, + { + "ImportPath": "github.com/hashicorp/go-checkpoint", + "Rev": "88326f6851319068e7b34981032128c0b1a6524d" + }, + { + "ImportPath": "github.com/hashicorp/go-multierror", + "Rev": "fcdddc395df1ddf4247c69bd436e84cfa0733f7e" + }, + { + "ImportPath": "github.com/hashicorp/go-version", + "Rev": "bb92dddfa9792e738a631f04ada52858a139bcf7" + }, + { + "ImportPath": "github.com/hashicorp/hcl", + "Rev": "513e04c400ee2e81e97f5e011c08fb42c6f69b84" + }, + { + "ImportPath": "github.com/hashicorp/yamux", + "Rev": "b4f943b3f25da97dec8e26bee1c3269019de070d" + }, + { + "ImportPath": "github.com/mitchellh/cli", + "Rev": "afc399c273e70173826fb6f518a48edff23fe897" + }, + { + "ImportPath": "github.com/mitchellh/colorstring", + "Rev": "61164e49940b423ba1f12ddbdf01632ac793e5e9" + }, + { + "ImportPath": "github.com/mitchellh/copystructure", + "Rev": "c101d94abf8cd5c6213c8300d0aed6368f2d6ede" + }, + { + "ImportPath": "github.com/mitchellh/go-homedir", + "Rev": "7d2d8c8a4e078ce3c58736ab521a40b37a504c52" + }, + { + "ImportPath": "github.com/mitchellh/mapstructure", + "Rev": "442e588f213303bec7936deba67901f8fc8f18b1" + }, + { + "ImportPath": "github.com/mitchellh/osext", + "Rev": "0dd3f918b21bec95ace9dc86c7e70266cfc5c702" + }, + { + "ImportPath": "github.com/mitchellh/panicwrap", + "Rev": "45cbfd3bae250c7676c077fb275be1a2968e066a" + }, + { + "ImportPath": "github.com/mitchellh/prefixedio", + "Rev": "89d9b535996bf0a185f85b59578f2e245f9e1724" + }, + { + "ImportPath": "github.com/mitchellh/reflectwalk", + "Rev": "9cdd861463675960a0a0083a7e2023e7b0c994d7" + } + ] +} From 2055405d2f03af73e4883231b913d76cba7990a9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Apr 2015 11:19:27 -0700 Subject: [PATCH 52/59] website: fix markdown syntax errors --- .../providers/openstack/r/blockstorage_volume_v1.html.markdown | 2 +- .../openstack/r/objectstorage_container_v1.html.markdown | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/docs/providers/openstack/r/blockstorage_volume_v1.html.markdown b/website/source/docs/providers/openstack/r/blockstorage_volume_v1.html.markdown index c547994cca4d..779d67e1c392 100644 --- a/website/source/docs/providers/openstack/r/blockstorage_volume_v1.html.markdown +++ b/website/source/docs/providers/openstack/r/blockstorage_volume_v1.html.markdown @@ -3,7 +3,7 @@ layout: "openstack" page_title: "OpenStack: openstack_blockstorage_volume_v1" sidebar_current: "docs-openstack-resource-blockstorage-volume-v1" description: |- -Manages a V1 volume resource within OpenStack. + Manages a V1 volume resource within OpenStack. --- # openstack\_blockstorage\_volume_v1 diff --git a/website/source/docs/providers/openstack/r/objectstorage_container_v1.html.markdown b/website/source/docs/providers/openstack/r/objectstorage_container_v1.html.markdown index 8101d1ca2153..d81eccc53180 100644 --- a/website/source/docs/providers/openstack/r/objectstorage_container_v1.html.markdown +++ b/website/source/docs/providers/openstack/r/objectstorage_container_v1.html.markdown @@ -3,7 +3,7 @@ layout: "openstack" page_title: "OpenStack: openstack_objectstorage_container_v1" sidebar_current: "docs-openstack-resource-objectstorage-container-v1" description: |- -Manages a V1 container resource within OpenStack. + Manages a V1 container resource within OpenStack. --- # openstack\_objectstorage\_container_v1 From 8577fd466c8269a783dfe855ea6e314056dfd873 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Apr 2015 11:29:46 -0700 Subject: [PATCH 53/59] website: update middleman dep --- website/Gemfile.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/website/Gemfile.lock b/website/Gemfile.lock index a55579e6dbf7..e863605326c9 100644 --- a/website/Gemfile.lock +++ b/website/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/hashicorp/middleman-hashicorp - revision: 783fe9517dd02badb85e5ddfeda4d8e35bbd05a8 + revision: fb03b8e60efc96f68c2dded0f49632fcd6eb6482 specs: middleman-hashicorp (0.1.0) bootstrap-sass (~> 3.3) @@ -20,16 +20,16 @@ GIT GEM remote: https://rubygems.org/ specs: - activesupport (4.1.9) + activesupport (4.1.10) i18n (~> 0.6, >= 0.6.9) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.1) tzinfo (~> 1.1) - autoprefixer-rails (5.1.7) + autoprefixer-rails (5.1.8) execjs json - bootstrap-sass (3.3.3) + bootstrap-sass (3.3.4.1) autoprefixer-rails (>= 5.0.0.1) sass (>= 3.2.19) builder (3.2.2) @@ -53,14 +53,14 @@ GEM sass (>= 3.3.0, < 3.5) compass-import-once (1.0.5) sass (>= 3.2, < 3.5) - daemons (1.1.9) + daemons (1.2.2) em-websocket (0.5.1) eventmachine (>= 0.12.9) http_parser.rb (~> 0.6.0) erubis (2.7.0) eventmachine (1.0.7) execjs (2.4.0) - ffi (1.9.6) + ffi (1.9.8) haml (4.0.6) tilt hike (1.2.3) @@ -75,8 +75,8 @@ GEM less (2.6.0) commonjs (~> 0.2.7) libv8 (3.16.14.7) - listen (2.8.5) - celluloid (>= 0.15.2) + listen (2.10.0) + celluloid (~> 0.16.0) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) middleman (3.3.10) @@ -159,7 +159,7 @@ GEM eventmachine (~> 1.0) rack (~> 1.0) thor (0.19.1) - thread_safe (0.3.4) + thread_safe (0.3.5) tilt (1.4.1) timers (4.0.1) hitimes From 2f37b80fb7e0f7ceee17885ed0f67549ebfb7f3f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Apr 2015 11:37:56 -0700 Subject: [PATCH 54/59] up version for dev --- CHANGELOG.md | 4 ++++ version.go | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dac6f49a6c5f..f717550a8edd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.1 (unreleased) + + + ## 0.4.0 (April 2, 2015) BACKWARDS INCOMPATIBILITIES: diff --git a/version.go b/version.go index bcabe0a71f85..9680a23b462b 100644 --- a/version.go +++ b/version.go @@ -4,9 +4,9 @@ package main var GitCommit string // The main version number that is being run at the moment. -const Version = "0.4.0" +const Version = "0.4.1" // A pre-release marker for the version. If this is "" (empty string) // then it means that it is a final release. Otherwise, this is a pre-release // such as "dev" (in development), "beta", "rc1", etc. -const VersionPrerelease = "" +const VersionPrerelease = "dev" From 3751533c3ad283c6bc9a7ad8cf8a60cd2d66de31 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Thu, 2 Apr 2015 14:10:16 -0500 Subject: [PATCH 55/59] Update CHANGELOG.md --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f717550a8edd..417ae3002c34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ BACKWARDS INCOMPATIBILITIES: consolidates all remote state management under one command. * Period-prefixed configuration files are now ignored. This might break existing Terraform configurations if you had period-prefixed files. + * The `block_device` attribute of `aws_instance` has been removed in favor + of three more specific attributes to specify block device mappings: + `root_block_device`, `ebs_block_device`, and `ephemeral_block_device`. + Configurations using the old attribute will generate a validation error + indicating that they must be updated to use the new fields [GH-1045]. FEATURES: @@ -67,6 +72,8 @@ IMPROVEMENTS: * provider/aws: Add non-destructive updates to AWS RDS. You can now upgrade `egine_version`, `parameter_group_name`, and `multi_az` without forcing a new database to be created.[GH-1341] + * providers/aws: Full support for block device mappings on instances and + launch configurations [GH-1045, GH-1364] * provisioners/remote-exec: SSH agent support. [GH-1208] BUG FIXES: From ccb6cefca93f1c61063a5907a132ace420471bb5 Mon Sep 17 00:00:00 2001 From: Jack Pearkes Date: Thu, 2 Apr 2015 21:08:42 -0700 Subject: [PATCH 56/59] website: fix openstack doc links and style --- website/source/assets/stylesheets/_docs.scss | 1 + website/source/layouts/docs.erb | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index 27cbba873c60..a2c9a55908b7 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -16,6 +16,7 @@ body.layout-cloudstack, body.layout-google, body.layout-heroku, body.layout-mailgun, +body.layout-openstack, body.layout-digitalocean, body.layout-aws, body.layout-docs, diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 30b8c3253719..a065d26a6920 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -155,6 +155,10 @@ > Mailgun + + > + OpenStack + From 268f935386236390d04ba9f889271cbdd8fb7e4e Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Fri, 3 Apr 2015 09:19:20 -0500 Subject: [PATCH 57/59] provider/aws: Fix issue finding db subnets AWS seems to lower case DB Subnet Group names, causing a failure in TF if your name isn't all lower case. --- .../aws/resource_aws_db_subnet_group.go | 18 +++++++++++++++--- .../aws/resource_aws_db_subnet_group_test.go | 8 +++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/builtin/providers/aws/resource_aws_db_subnet_group.go b/builtin/providers/aws/resource_aws_db_subnet_group.go index a7bbc7b47d26..1c1b49a710aa 100644 --- a/builtin/providers/aws/resource_aws_db_subnet_group.go +++ b/builtin/providers/aws/resource_aws_db_subnet_group.go @@ -3,6 +3,7 @@ package aws import ( "fmt" "log" + "strings" "time" "github.com/hashicorp/aws-sdk-go/aws" @@ -87,12 +88,23 @@ func resourceAwsDbSubnetGroupRead(d *schema.ResourceData, meta interface{}) erro return err } - if len(describeResp.DBSubnetGroups) != 1 || - *describeResp.DBSubnetGroups[0].DBSubnetGroupName != d.Id() { + if len(describeResp.DBSubnetGroups) == 0 { return fmt.Errorf("Unable to find DB Subnet Group: %#v", describeResp.DBSubnetGroups) } - subnetGroup := describeResp.DBSubnetGroups[0] + var subnetGroup rds.DBSubnetGroup + for _, s := range describeResp.DBSubnetGroups { + // AWS is down casing the name provided, so we compare lower case versions + // of the names. We lower case both our name and their name in the check, + // incase they change that someday. + if strings.ToLower(d.Id()) == strings.ToLower(*s.DBSubnetGroupName) { + subnetGroup = describeResp.DBSubnetGroups[0] + } + } + + if subnetGroup.DBSubnetGroupName == nil { + return fmt.Errorf("Unable to find DB Subnet Group: %#v", describeResp.DBSubnetGroups) + } d.Set("name", *subnetGroup.DBSubnetGroupName) d.Set("description", *subnetGroup.DBSubnetGroupDescription) diff --git a/builtin/providers/aws/resource_aws_db_subnet_group_test.go b/builtin/providers/aws/resource_aws_db_subnet_group_test.go index 2bee3a3ff0df..dd4b2d58f6ed 100644 --- a/builtin/providers/aws/resource_aws_db_subnet_group_test.go +++ b/builtin/providers/aws/resource_aws_db_subnet_group_test.go @@ -103,16 +103,22 @@ resource "aws_subnet" "foo" { cidr_block = "10.1.1.0/24" availability_zone = "us-west-2a" vpc_id = "${aws_vpc.foo.id}" + tags { + Name = "tf-dbsubnet-test-1" + } } resource "aws_subnet" "bar" { cidr_block = "10.1.2.0/24" availability_zone = "us-west-2b" vpc_id = "${aws_vpc.foo.id}" + tags { + Name = "tf-dbsubnet-test-2" + } } resource "aws_db_subnet_group" "foo" { - name = "foo" + name = "FOO" description = "foo description" subnet_ids = ["${aws_subnet.foo.id}", "${aws_subnet.bar.id}"] } From 66dbf91ffd37948c05c36a227c35e869c2cc2897 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Fri, 3 Apr 2015 09:57:30 -0500 Subject: [PATCH 58/59] helper/schema: ensure ForceNew set when Update is not If a given resource does not define an `Update` function, then all of its attributes must be specified as `ForceNew`, lest Applys fail with "doesn't support update" like #1367. This is something we can detect automatically, so this adds a check for it when we validate provider implementations. --- .../aws/resource_aws_launch_configuration.go | 2 ++ .../google/resource_compute_route.go | 1 + helper/schema/resource.go | 22 +++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/builtin/providers/aws/resource_aws_launch_configuration.go b/builtin/providers/aws/resource_aws_launch_configuration.go index 854c0feb9157..a0a86a66f699 100644 --- a/builtin/providers/aws/resource_aws_launch_configuration.go +++ b/builtin/providers/aws/resource_aws_launch_configuration.go @@ -84,6 +84,7 @@ func resourceAwsLaunchConfiguration() *schema.Resource { "associate_public_ip_address": &schema.Schema{ Type: schema.TypeBool, Optional: true, + ForceNew: true, Default: false, }, @@ -96,6 +97,7 @@ func resourceAwsLaunchConfiguration() *schema.Resource { "ebs_optimized": &schema.Schema{ Type: schema.TypeBool, Optional: true, + ForceNew: true, Default: false, }, diff --git a/builtin/providers/google/resource_compute_route.go b/builtin/providers/google/resource_compute_route.go index aec9e8d3d958..1f52a2807bc2 100644 --- a/builtin/providers/google/resource_compute_route.go +++ b/builtin/providers/google/resource_compute_route.go @@ -75,6 +75,7 @@ func resourceComputeRoute() *schema.Resource { "tags": &schema.Schema{ Type: schema.TypeSet, Optional: true, + ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: func(v interface{}) int { return hashcode.String(v.(string)) diff --git a/helper/schema/resource.go b/helper/schema/resource.go index 797d021ab65e..0c640e697cc9 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -225,9 +225,31 @@ func (r *Resource) InternalValidate() error { return errors.New("resource is nil") } + if r.isTopLevel() { + // All non-Computed attributes must be ForceNew if Update is not defined + if r.Update == nil { + nonForceNewAttrs := make([]string, 0) + for k, v := range r.Schema { + if !v.ForceNew && !v.Computed { + nonForceNewAttrs = append(nonForceNewAttrs, k) + } + } + if len(nonForceNewAttrs) > 0 { + return fmt.Errorf( + "No Update defined, must set ForceNew on: %#v", nonForceNewAttrs) + } + } + } + return schemaMap(r.Schema).InternalValidate() } +// Returns true if the resource is "top level" i.e. not a sub-resource. +func (r *Resource) isTopLevel() bool { + // TODO: This is a heuristic; replace with a definitive attribute? + return r.Create != nil +} + // Determines if a given InstanceState needs to be migrated by checking the // stored version number with the current SchemaVersion func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) { From fdfb84df71182fedd480b5c529d2430abaa44452 Mon Sep 17 00:00:00 2001 From: Alex Shadrin Date: Sat, 4 Apr 2015 13:06:27 +0300 Subject: [PATCH 59/59] added taint completion --- contrib/zsh-completion/_terraform | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/contrib/zsh-completion/_terraform b/contrib/zsh-completion/_terraform index 67896b0fcbd3..f1abf535dc1b 100644 --- a/contrib/zsh-completion/_terraform +++ b/contrib/zsh-completion/_terraform @@ -13,6 +13,7 @@ _terraform_cmds=( 'push:Uploads the the local state to the remote server' 'refresh:Update local state file against real resources' 'remote:Configures remote state management' + 'taint:Manualy forcing a destroy and recreate on the next plan/apply' 'show:Inspect Terraform state or plan' 'version:Prints the Terraform version' ) @@ -95,6 +96,16 @@ __refresh() { '-var-file=[(path) Set variables in the Terraform configuration from a file. If "terraform.tfvars" is present, it will be automatically loaded if this flag is not specified.]' } +__taint() { + _arguments \ + '-allow-missing[If specified, the command will succeed (exit code 0) even if the resource is missing.]' \ + '-backup=[(path) Path to backup the existing state file before modifying. Defaults to the "-state-out" path with ".backup" extension. Set to "-" to disable backup.]' \ + '-module=[(path) The module path where the resource lives. By default this will be root. Child modules can be specified by names. Ex. "consul" or "consul.vpc" (nested modules).]' \ + '-no-color[If specified, output will not contain any color.]' \ + '-state=[(path) Path to read and save state (unless state-out is specified). Defaults to "terraform.tfstate".]' \ + '-state-out=[(path) Path to write updated state file. By default, the "-state" path will be used.]' +} + __remote() { _arguments \ '-address=[(url) URL of the remote storage server. Required for HTTP backend, optional for Atlas and Consul.]' \ @@ -104,7 +115,7 @@ __remote() { '-disable[Disables remote state management and migrates the state to the -state path.]' \ '-name=[(name) Name of the state file in the state storage server. Required for Atlas backend.]' \ '-path=[(path) Path of the remote state in Consul. Required for the Consul backend.]' \ - '-pull=[(true) Controls if the remote state is pulled before disabling. This defaults to true to ensure the latest state is cached before disabling.]'\ + '-pull=[(true) Controls if the remote state is pulled before disabling. This defaults to true to ensure the latest state is cached before disabling.]' \ '-state=[(path) Path to read and save state (unless state-out is specified). Defaults to "terraform.tfstate".]' } @@ -145,4 +156,6 @@ case "$words[1]" in __remote ;; show) __show ;; + taint) + __taint ;; esac