diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8720a0888a..29b8554fae 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -373,7 +373,7 @@ to a single resource. Most tests follow a similar structure. 1. Pre-flight checks are made to ensure that sufficient provider configuration is available to be able to proceed - for example in an acceptance test - targetting AWS, `AWS_ACCESS_KEY_ID` and `AWS_SECRET_KEY` must be set prior + targetting AWS, `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` must be set prior to running acceptance tests. This is common to all tests exercising a single provider. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6465ec47b0..20f7a236af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,19 @@ ## 0.7.1 (Unreleased) FEATURES: + * **New Resource:** `aws_vpn_gateway_attachment` [GH-7870] IMPROVEMENTS: * provider/vsphere: Improved SCSI controller handling in `vsphere_virtual_machine` [GH-7908] + * provider/aws: Introduce `aws_elasticsearch_domain` `elasticsearch_version` field (to specify ES version) [GH-7860] BUG FIXES: * provider/aws: guard against missing image_digest in `aws_ecs_task_definition` [GH-7966] + * provider/aws: `aws_cloudformation_stack` now respects `timeout_in_minutes` field when waiting for CF API to finish an update operation [GH-7997] * provider/aws: Add state filter to `aws_availability_zone`s data source [GH-7965] * provider/aws: Handle lack of snapshot ID for a volume in `ami_copy` [GH-7995] * provider/aws: Retry association of IAM Role & instance profile [GH-7938] + * provider/aws: Fix `aws_s3_bucket` resource `redirect_all_requests_to` action [GH-7883] * provider/google: Use resource specific project when making queries/changes [GH-7029] ## 0.7.0 (August 2, 2016) diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 69e264dd92..234a9c90e6 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -282,6 +282,7 @@ func Provider() terraform.ResourceProvider { "aws_vpn_connection": resourceAwsVpnConnection(), "aws_vpn_connection_route": resourceAwsVpnConnectionRoute(), "aws_vpn_gateway": resourceAwsVpnGateway(), + "aws_vpn_gateway_attachment": resourceAwsVpnGatewayAttachment(), }, ConfigureFunc: providerConfigure, } diff --git a/builtin/providers/aws/resource_aws_cloudformation_stack.go b/builtin/providers/aws/resource_aws_cloudformation_stack.go index 28935e33ce..56249587b7 100644 --- a/builtin/providers/aws/resource_aws_cloudformation_stack.go +++ b/builtin/providers/aws/resource_aws_cloudformation_stack.go @@ -268,6 +268,7 @@ func resourceAwsCloudFormationStackRead(d *schema.ResourceData, meta interface{} } func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface{}) error { + retryTimeout := int64(30) conn := meta.(*AWSClient).cfconn input := &cloudformation.UpdateStackInput{ @@ -314,6 +315,13 @@ func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface return err } + if v, ok := d.GetOk("timeout_in_minutes"); ok { + m := int64(v.(int)) + if m > retryTimeout { + retryTimeout = m + 5 + log.Printf("[DEBUG] CloudFormation timeout: %d", retryTimeout) + } + } wait := resource.StateChangeConf{ Pending: []string{ "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", @@ -323,7 +331,7 @@ func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface "UPDATE_ROLLBACK_COMPLETE", }, Target: []string{"UPDATE_COMPLETE"}, - Timeout: 15 * time.Minute, + Timeout: time.Duration(retryTimeout) * time.Minute, MinTimeout: 5 * time.Second, Refresh: func() (interface{}, string, error) { resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{ diff --git a/builtin/providers/aws/resource_aws_elasticsearch_domain.go b/builtin/providers/aws/resource_aws_elasticsearch_domain.go index 35bffc89a5..b7ba0a843e 100644 --- a/builtin/providers/aws/resource_aws_elasticsearch_domain.go +++ b/builtin/providers/aws/resource_aws_elasticsearch_domain.go @@ -129,6 +129,13 @@ func resourceAwsElasticSearchDomain() *schema.Resource { }, }, }, + "elasticsearch_version": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "1.5", + ForceNew: true, + }, + "tags": tagsSchema(), }, } @@ -138,7 +145,8 @@ func resourceAwsElasticSearchDomainCreate(d *schema.ResourceData, meta interface conn := meta.(*AWSClient).esconn input := elasticsearch.CreateElasticsearchDomainInput{ - DomainName: aws.String(d.Get("domain_name").(string)), + DomainName: aws.String(d.Get("domain_name").(string)), + ElasticsearchVersion: aws.String(d.Get("elasticsearch_version").(string)), } if v, ok := d.GetOk("access_policies"); ok { @@ -262,8 +270,9 @@ func resourceAwsElasticSearchDomainRead(d *schema.ResourceData, meta interface{} if err != nil { return err } - d.Set("domain_id", *ds.DomainId) - d.Set("domain_name", *ds.DomainName) + d.Set("domain_id", ds.DomainId) + d.Set("domain_name", ds.DomainName) + d.Set("elasticsearch_version", ds.ElasticsearchVersion) if ds.Endpoint != nil { d.Set("endpoint", *ds.Endpoint) } @@ -282,7 +291,7 @@ func resourceAwsElasticSearchDomainRead(d *schema.ResourceData, meta interface{} }) } - d.Set("arn", *ds.ARN) + d.Set("arn", ds.ARN) listOut, err := conn.ListTags(&elasticsearch.ListTagsInput{ ARN: ds.ARN, diff --git a/builtin/providers/aws/resource_aws_elasticsearch_domain_test.go b/builtin/providers/aws/resource_aws_elasticsearch_domain_test.go index 881d923223..85dd372897 100644 --- a/builtin/providers/aws/resource_aws_elasticsearch_domain_test.go +++ b/builtin/providers/aws/resource_aws_elasticsearch_domain_test.go @@ -7,12 +7,14 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" elasticsearch "github.com/aws/aws-sdk-go/service/elasticsearchservice" + "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) func TestAccAWSElasticSearchDomain_basic(t *testing.T) { var domain elasticsearch.ElasticsearchDomainStatus + ri := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -20,9 +22,32 @@ func TestAccAWSElasticSearchDomain_basic(t *testing.T) { CheckDestroy: testAccCheckESDomainDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccESDomainConfig, + Config: testAccESDomainConfig(ri), Check: resource.ComposeTestCheckFunc( testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), + resource.TestCheckResourceAttr( + "aws_elasticsearch_domain.example", "elasticsearch_version", "1.5"), + ), + }, + }, + }) +} + +func TestAccAWSElasticSearchDomain_v23(t *testing.T) { + var domain elasticsearch.ElasticsearchDomainStatus + ri := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckESDomainDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccESDomainConfigV23(ri), + Check: resource.ComposeTestCheckFunc( + testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), + resource.TestCheckResourceAttr( + "aws_elasticsearch_domain.example", "elasticsearch_version", "2.3"), ), }, }, @@ -31,6 +56,7 @@ func TestAccAWSElasticSearchDomain_basic(t *testing.T) { func TestAccAWSElasticSearchDomain_complex(t *testing.T) { var domain elasticsearch.ElasticsearchDomainStatus + ri := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -38,7 +64,7 @@ func TestAccAWSElasticSearchDomain_complex(t *testing.T) { CheckDestroy: testAccCheckESDomainDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccESDomainConfig_complex, + Config: testAccESDomainConfig_complex(ri), Check: resource.ComposeTestCheckFunc( testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), ), @@ -50,6 +76,7 @@ func TestAccAWSElasticSearchDomain_complex(t *testing.T) { func TestAccAWSElasticSearch_tags(t *testing.T) { var domain elasticsearch.ElasticsearchDomainStatus var td elasticsearch.ListTagsOutput + ri := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -57,14 +84,14 @@ func TestAccAWSElasticSearch_tags(t *testing.T) { CheckDestroy: testAccCheckAWSELBDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccESDomainConfig, + Config: testAccESDomainConfig(ri), Check: resource.ComposeTestCheckFunc( testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), ), }, resource.TestStep{ - Config: testAccESDomainConfig_TagUpdate, + Config: testAccESDomainConfig_TagUpdate(ri), Check: resource.ComposeTestCheckFunc( testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), testAccLoadESTags(&domain, &td), @@ -144,26 +171,31 @@ func testAccCheckESDomainDestroy(s *terraform.State) error { return nil } -const testAccESDomainConfig = ` +func testAccESDomainConfig(randInt int) string { + return fmt.Sprintf(` resource "aws_elasticsearch_domain" "example" { - domain_name = "tf-test-1" + domain_name = "tf-test-%d" +} +`, randInt) } -` -const testAccESDomainConfig_TagUpdate = ` +func testAccESDomainConfig_TagUpdate(randInt int) string { + return fmt.Sprintf(` resource "aws_elasticsearch_domain" "example" { - domain_name = "tf-test-1" + domain_name = "tf-test-%d" tags { foo = "bar" new = "type" } } -` +`, randInt) +} -const testAccESDomainConfig_complex = ` +func testAccESDomainConfig_complex(randInt int) string { + return fmt.Sprintf(` resource "aws_elasticsearch_domain" "example" { - domain_name = "tf-test-2" + domain_name = "tf-test-%d" advanced_options { "indices.fielddata.cache.size" = 80 @@ -186,4 +218,14 @@ resource "aws_elasticsearch_domain" "example" { bar = "complex" } } -` +`, randInt) +} + +func testAccESDomainConfigV23(randInt int) string { + return fmt.Sprintf(` +resource "aws_elasticsearch_domain" "example" { + domain_name = "tf-test-%d" + elasticsearch_version = "2.3" +} +`, randInt) +} diff --git a/builtin/providers/aws/resource_aws_s3_bucket.go b/builtin/providers/aws/resource_aws_s3_bucket.go index 6897f1e7f8..29ac708f5d 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket.go +++ b/builtin/providers/aws/resource_aws_s3_bucket.go @@ -495,8 +495,20 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error { if v.Protocol == nil { w["redirect_all_requests_to"] = *v.HostName } else { + var host string + var path string + parsedHostName, err := url.Parse(*v.HostName) + if err == nil { + host = parsedHostName.Host + path = parsedHostName.Path + } else { + host = *v.HostName + path = "" + } + w["redirect_all_requests_to"] = (&url.URL{ - Host: *v.HostName, + Host: host, + Path: path, Scheme: *v.Protocol, }).String() } @@ -947,7 +959,12 @@ func resourceAwsS3BucketWebsitePut(s3conn *s3.S3, d *schema.ResourceData, websit if redirectAllRequestsTo != "" { redirect, err := url.Parse(redirectAllRequestsTo) if err == nil && redirect.Scheme != "" { - websiteConfiguration.RedirectAllRequestsTo = &s3.RedirectAllRequestsTo{HostName: aws.String(redirect.Host), Protocol: aws.String(redirect.Scheme)} + var redirectHostBuf bytes.Buffer + redirectHostBuf.WriteString(redirect.Host) + if redirect.Path != "" { + redirectHostBuf.WriteString(redirect.Path) + } + websiteConfiguration.RedirectAllRequestsTo = &s3.RedirectAllRequestsTo{HostName: aws.String(redirectHostBuf.String()), Protocol: aws.String(redirect.Scheme)} } else { websiteConfiguration.RedirectAllRequestsTo = &s3.RedirectAllRequestsTo{HostName: aws.String(redirectAllRequestsTo)} } diff --git a/builtin/providers/aws/resource_aws_vpn_gateway.go b/builtin/providers/aws/resource_aws_vpn_gateway.go index 27f4a45f75..845e11cd37 100644 --- a/builtin/providers/aws/resource_aws_vpn_gateway.go +++ b/builtin/providers/aws/resource_aws_vpn_gateway.go @@ -32,6 +32,7 @@ func resourceAwsVpnGateway() *schema.Resource { "vpc_id": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, }, "tags": tagsSchema(), @@ -80,17 +81,18 @@ func resourceAwsVpnGatewayRead(d *schema.ResourceData, meta interface{}) error { } vpnGateway := resp.VpnGateways[0] - if vpnGateway == nil { + if vpnGateway == nil || *vpnGateway.State == "deleted" { // Seems we have lost our VPN gateway d.SetId("") return nil } - if len(vpnGateway.VpcAttachments) == 0 || *vpnGateway.VpcAttachments[0].State == "detached" || *vpnGateway.VpcAttachments[0].State == "deleted" { + vpnAttachment := vpnGatewayGetAttachment(vpnGateway) + if len(vpnGateway.VpcAttachments) == 0 || *vpnAttachment.State == "detached" { // Gateway exists but not attached to the VPC d.Set("vpc_id", "") } else { - d.Set("vpc_id", vpnGateway.VpcAttachments[0].VpcId) + d.Set("vpc_id", *vpnAttachment.VpcId) } d.Set("availability_zone", vpnGateway.AvailabilityZone) d.Set("tags", tagsToMap(vpnGateway.Tags)) @@ -301,12 +303,21 @@ func vpnGatewayAttachStateRefreshFunc(conn *ec2.EC2, id string, expected string) } vpnGateway := resp.VpnGateways[0] - if len(vpnGateway.VpcAttachments) == 0 { // No attachments, we're detached return vpnGateway, "detached", nil } - return vpnGateway, *vpnGateway.VpcAttachments[0].State, nil + vpnAttachment := vpnGatewayGetAttachment(vpnGateway) + return vpnGateway, *vpnAttachment.State, nil } } + +func vpnGatewayGetAttachment(vgw *ec2.VpnGateway) *ec2.VpcAttachment { + for _, v := range vgw.VpcAttachments { + if *v.State == "attached" { + return v + } + } + return &ec2.VpcAttachment{State: aws.String("detached")} +} diff --git a/builtin/providers/aws/resource_aws_vpn_gateway_attachment.go b/builtin/providers/aws/resource_aws_vpn_gateway_attachment.go new file mode 100644 index 0000000000..b19393bfb9 --- /dev/null +++ b/builtin/providers/aws/resource_aws_vpn_gateway_attachment.go @@ -0,0 +1,210 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsVpnGatewayAttachment() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsVpnGatewayAttachmentCreate, + Read: resourceAwsVpnGatewayAttachmentRead, + Delete: resourceAwsVpnGatewayAttachmentDelete, + + Schema: map[string]*schema.Schema{ + "vpc_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "vpn_gateway_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsVpnGatewayAttachmentCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + vpcId := d.Get("vpc_id").(string) + vgwId := d.Get("vpn_gateway_id").(string) + + createOpts := &ec2.AttachVpnGatewayInput{ + VpcId: aws.String(vpcId), + VpnGatewayId: aws.String(vgwId), + } + log.Printf("[DEBUG] VPN Gateway attachment options: %#v", *createOpts) + + _, err := conn.AttachVpnGateway(createOpts) + if err != nil { + return fmt.Errorf("Error attaching VPN Gateway %q to VPC %q: %s", + vgwId, vpcId, err) + } + + d.SetId(vpnGatewayAttachmentId(vpcId, vgwId)) + log.Printf("[INFO] VPN Gateway %q attachment ID: %s", vgwId, d.Id()) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"detached", "attaching"}, + Target: []string{"attached"}, + Refresh: vpnGatewayAttachmentStateRefresh(conn, vpcId, vgwId), + Timeout: 5 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for VPN Gateway %q to attach to VPC %q: %s", + vgwId, vpcId, err) + } + log.Printf("[DEBUG] VPN Gateway %q attached to VPC %q.", vgwId, vpcId) + + return resourceAwsVpnGatewayAttachmentRead(d, meta) +} + +func resourceAwsVpnGatewayAttachmentRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + vgwId := d.Get("vpn_gateway_id").(string) + + resp, err := conn.DescribeVpnGateways(&ec2.DescribeVpnGatewaysInput{ + VpnGatewayIds: []*string{aws.String(vgwId)}, + }) + + if err != nil { + awsErr, ok := err.(awserr.Error) + if ok && awsErr.Code() == "InvalidVPNGatewayID.NotFound" { + log.Printf("[WARN] VPN Gateway %q not found.", vgwId) + d.SetId("") + return nil + } + return err + } + + vgw := resp.VpnGateways[0] + if *vgw.State == "deleted" { + log.Printf("[INFO] VPN Gateway %q appears to have been deleted.", vgwId) + d.SetId("") + return nil + } + + vga := vpnGatewayGetAttachment(vgw) + if len(vgw.VpcAttachments) == 0 || *vga.State == "detached" { + d.Set("vpc_id", "") + return nil + } + + d.Set("vpc_id", *vga.VpcId) + return nil +} + +func resourceAwsVpnGatewayAttachmentDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + vpcId := d.Get("vpc_id").(string) + vgwId := d.Get("vpn_gateway_id").(string) + + if vpcId == "" { + log.Printf("[DEBUG] Not detaching VPN Gateway %q as no VPC ID is set.", vgwId) + return nil + } + + _, err := conn.DetachVpnGateway(&ec2.DetachVpnGatewayInput{ + VpcId: aws.String(vpcId), + VpnGatewayId: aws.String(vgwId), + }) + + if err != nil { + awsErr, ok := err.(awserr.Error) + if ok { + switch awsErr.Code() { + case "InvalidVPNGatewayID.NotFound": + log.Printf("[WARN] VPN Gateway %q not found.", vgwId) + d.SetId("") + return nil + case "InvalidVpnGatewayAttachment.NotFound": + log.Printf( + "[WARN] VPN Gateway %q attachment to VPC %q not found.", + vgwId, vpcId) + d.SetId("") + return nil + } + } + + return fmt.Errorf("Error detaching VPN Gateway %q from VPC %q: %s", + vgwId, vpcId, err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"attached", "detaching"}, + Target: []string{"detached"}, + Refresh: vpnGatewayAttachmentStateRefresh(conn, vpcId, vgwId), + Timeout: 5 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for VPN Gateway %q to detach from VPC %q: %s", + vgwId, vpcId, err) + } + log.Printf("[DEBUG] VPN Gateway %q detached from VPC %q.", vgwId, vpcId) + + d.SetId("") + return nil +} + +func vpnGatewayAttachmentStateRefresh(conn *ec2.EC2, vpcId, vgwId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + resp, err := conn.DescribeVpnGateways(&ec2.DescribeVpnGatewaysInput{ + Filters: []*ec2.Filter{ + &ec2.Filter{ + Name: aws.String("attachment.vpc-id"), + Values: []*string{aws.String(vpcId)}, + }, + }, + VpnGatewayIds: []*string{aws.String(vgwId)}, + }) + + if err != nil { + awsErr, ok := err.(awserr.Error) + if ok { + switch awsErr.Code() { + case "InvalidVPNGatewayID.NotFound": + fallthrough + case "InvalidVpnGatewayAttachment.NotFound": + return nil, "", nil + } + } + + return nil, "", err + } + + vgw := resp.VpnGateways[0] + if len(vgw.VpcAttachments) == 0 { + return vgw, "detached", nil + } + + vga := vpnGatewayGetAttachment(vgw) + + log.Printf("[DEBUG] VPN Gateway %q attachment status: %s", vgwId, *vga.State) + return vgw, *vga.State, nil + } +} + +func vpnGatewayAttachmentId(vpcId, vgwId string) string { + return fmt.Sprintf("vpn-attachment-%x", hashcode.String(fmt.Sprintf("%s-%s", vpcId, vgwId))) +} diff --git a/builtin/providers/aws/resource_aws_vpn_gateway_attachment_test.go b/builtin/providers/aws/resource_aws_vpn_gateway_attachment_test.go new file mode 100644 index 0000000000..5f12d6fb84 --- /dev/null +++ b/builtin/providers/aws/resource_aws_vpn_gateway_attachment_test.go @@ -0,0 +1,163 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSVpnGatewayAttachment_basic(t *testing.T) { + var vpc ec2.Vpc + var vgw ec2.VpnGateway + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "aws_vpn_gateway_attachment.test", + Providers: testAccProviders, + CheckDestroy: testAccCheckVpnGatewayAttachmentDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccVpnGatewayAttachmentConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists( + "aws_vpc.test", + &vpc), + testAccCheckVpnGatewayExists( + "aws_vpn_gateway.test", + &vgw), + testAccCheckVpnGatewayAttachmentExists( + "aws_vpn_gateway_attachment.test", + &vpc, &vgw), + ), + }, + }, + }) +} + +func TestAccAWSVpnGatewayAttachment_deleted(t *testing.T) { + var vpc ec2.Vpc + var vgw ec2.VpnGateway + + testDeleted := func(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[n] + if ok { + return fmt.Errorf("Expected VPN Gateway attachment resource %q to be deleted.", n) + } + return nil + } + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "aws_vpn_gateway_attachment.test", + Providers: testAccProviders, + CheckDestroy: testAccCheckVpnGatewayAttachmentDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccVpnGatewayAttachmentConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists( + "aws_vpc.test", + &vpc), + testAccCheckVpnGatewayExists( + "aws_vpn_gateway.test", + &vgw), + testAccCheckVpnGatewayAttachmentExists( + "aws_vpn_gateway_attachment.test", + &vpc, &vgw), + ), + }, + resource.TestStep{ + Config: testAccNoVpnGatewayAttachmentConfig, + Check: resource.ComposeTestCheckFunc( + testDeleted("aws_vpn_gateway_attachment.test"), + ), + }, + }, + }) +} + +func testAccCheckVpnGatewayAttachmentExists(n string, vpc *ec2.Vpc, vgw *ec2.VpnGateway) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + vpcId := rs.Primary.Attributes["vpc_id"] + vgwId := rs.Primary.Attributes["vpn_gateway_id"] + + if len(vgw.VpcAttachments) == 0 { + return fmt.Errorf("VPN Gateway %q has no attachments.", vgwId) + } + + if *vgw.VpcAttachments[0].State != "attached" { + return fmt.Errorf("Expected VPN Gateway %q to be in attached state, but got: %q", + vgwId, *vgw.VpcAttachments[0].State) + } + + if *vgw.VpcAttachments[0].VpcId != *vpc.VpcId { + return fmt.Errorf("Expected VPN Gateway %q to be attached to VPC %q, but got: %q", + vgwId, vpcId, *vgw.VpcAttachments[0].VpcId) + } + + return nil + } +} + +func testAccCheckVpnGatewayAttachmentDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpn_gateway_attachment" { + continue + } + + vgwId := rs.Primary.Attributes["vpn_gateway_id"] + + resp, err := conn.DescribeVpnGateways(&ec2.DescribeVpnGatewaysInput{ + VpnGatewayIds: []*string{aws.String(vgwId)}, + }) + if err != nil { + return err + } + + vgw := resp.VpnGateways[0] + if *vgw.VpcAttachments[0].State != "detached" { + return fmt.Errorf("Expected VPN Gateway %q to be in detached state, but got: %q", + vgwId, *vgw.VpcAttachments[0].State) + } + } + + return nil +} + +const testAccNoVpnGatewayAttachmentConfig = ` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_vpn_gateway" "test" { } +` + +const testAccVpnGatewayAttachmentConfig = ` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_vpn_gateway" "test" { } + +resource "aws_vpn_gateway_attachment" "test" { + vpc_id = "${aws_vpc.test.id}" + vpn_gateway_id = "${aws_vpn_gateway.test.id}" +} +` diff --git a/builtin/providers/aws/resource_aws_vpn_gateway_test.go b/builtin/providers/aws/resource_aws_vpn_gateway_test.go index 0e3677d6f9..c9d2d921ac 100644 --- a/builtin/providers/aws/resource_aws_vpn_gateway_test.go +++ b/builtin/providers/aws/resource_aws_vpn_gateway_test.go @@ -16,10 +16,10 @@ func TestAccAWSVpnGateway_basic(t *testing.T) { testNotEqual := func(*terraform.State) error { if len(v.VpcAttachments) == 0 { - return fmt.Errorf("VPN gateway A is not attached") + return fmt.Errorf("VPN Gateway A is not attached") } if len(v2.VpcAttachments) == 0 { - return fmt.Errorf("VPN gateway B is not attached") + return fmt.Errorf("VPN Gateway B is not attached") } id1 := v.VpcAttachments[0].VpcId @@ -58,20 +58,38 @@ func TestAccAWSVpnGateway_basic(t *testing.T) { } func TestAccAWSVpnGateway_reattach(t *testing.T) { - var v ec2.VpnGateway + var vpc1, vpc2 ec2.Vpc + var vgw1, vgw2 ec2.VpnGateway - genTestStateFunc := func(expectedState string) func(*terraform.State) error { + testAttachmentFunc := func(vgw *ec2.VpnGateway, vpc *ec2.Vpc) func(*terraform.State) error { return func(*terraform.State) error { - if len(v.VpcAttachments) == 0 { - if expectedState != "detached" { - return fmt.Errorf("VPN gateway has no VPC attachments") + if len(vgw.VpcAttachments) == 0 { + return fmt.Errorf("VPN Gateway %q has no VPC attachments.", + *vgw.VpnGatewayId) + } + + if len(vgw.VpcAttachments) > 1 { + count := 0 + for _, v := range vgw.VpcAttachments { + if *v.State == "attached" { + count += 1 + } } - } else if len(v.VpcAttachments) == 1 { - if *v.VpcAttachments[0].State != expectedState { - return fmt.Errorf("Expected VPC gateway VPC attachment to be in '%s' state, but was not: %s", expectedState, v) + if count > 1 { + return fmt.Errorf( + "VPN Gateway %q has an unexpected number of VPC attachments (more than 1): %#v", + *vgw.VpnGatewayId, vgw.VpcAttachments) } - } else { - return fmt.Errorf("VPN gateway has unexpected number of VPC attachments(more than 1): %s", v) + } + + if *vgw.VpcAttachments[0].State != "attached" { + return fmt.Errorf("Expected VPN Gateway %q to be attached.", + *vgw.VpnGatewayId) + } + + if *vgw.VpcAttachments[0].VpcId != *vpc.VpcId { + return fmt.Errorf("Expected VPN Gateway %q to be attached to VPC %q, but got: %q", + *vgw.VpnGatewayId, *vpc.VpcId, *vgw.VpcAttachments[0].VpcId) } return nil } @@ -84,27 +102,38 @@ func TestAccAWSVpnGateway_reattach(t *testing.T) { CheckDestroy: testAccCheckVpnGatewayDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccVpnGatewayConfig, + Config: testAccCheckVpnGatewayConfigReattach, Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists("aws_vpc.foo", &vpc1), + testAccCheckVpcExists("aws_vpc.bar", &vpc2), testAccCheckVpnGatewayExists( - "aws_vpn_gateway.foo", &v), - genTestStateFunc("attached"), + "aws_vpn_gateway.foo", &vgw1), + testAccCheckVpnGatewayExists( + "aws_vpn_gateway.bar", &vgw2), + testAttachmentFunc(&vgw1, &vpc1), + testAttachmentFunc(&vgw2, &vpc2), ), }, resource.TestStep{ - Config: testAccVpnGatewayConfigDetach, + Config: testAccCheckVpnGatewayConfigReattachChange, Check: resource.ComposeTestCheckFunc( testAccCheckVpnGatewayExists( - "aws_vpn_gateway.foo", &v), - genTestStateFunc("detached"), + "aws_vpn_gateway.foo", &vgw1), + testAccCheckVpnGatewayExists( + "aws_vpn_gateway.bar", &vgw2), + testAttachmentFunc(&vgw2, &vpc1), + testAttachmentFunc(&vgw1, &vpc2), ), }, resource.TestStep{ - Config: testAccVpnGatewayConfig, + Config: testAccCheckVpnGatewayConfigReattach, Check: resource.ComposeTestCheckFunc( testAccCheckVpnGatewayExists( - "aws_vpn_gateway.foo", &v), - genTestStateFunc("attached"), + "aws_vpn_gateway.foo", &vgw1), + testAccCheckVpnGatewayExists( + "aws_vpn_gateway.bar", &vgw2), + testAttachmentFunc(&vgw1, &vpc1), + testAttachmentFunc(&vgw2, &vpc2), ), }, }, @@ -118,7 +147,7 @@ func TestAccAWSVpnGateway_delete(t *testing.T) { return func(s *terraform.State) error { _, ok := s.RootModule().Resources[r] if ok { - return fmt.Errorf("VPN Gateway %q should have been deleted", r) + return fmt.Errorf("VPN Gateway %q should have been deleted.", r) } return nil } @@ -159,7 +188,6 @@ func TestAccAWSVpnGateway_tags(t *testing.T) { testAccCheckTags(&v.Tags, "foo", "bar"), ), }, - resource.TestStep{ Config: testAccCheckVpnGatewayConfigTagsUpdate, Check: resource.ComposeTestCheckFunc( @@ -198,7 +226,7 @@ func testAccCheckVpnGatewayDestroy(s *terraform.State) error { } if *v.State != "deleted" { - return fmt.Errorf("Expected VpnGateway to be in deleted state, but was not: %s", v) + return fmt.Errorf("Expected VPN Gateway to be in deleted state, but was not: %s", v) } return nil } @@ -235,7 +263,7 @@ func testAccCheckVpnGatewayExists(n string, ig *ec2.VpnGateway) resource.TestChe return err } if len(resp.VpnGateways) == 0 { - return fmt.Errorf("VPNGateway not found") + return fmt.Errorf("VPN Gateway not found") } *ig = *resp.VpnGateways[0] @@ -270,16 +298,6 @@ resource "aws_vpn_gateway" "foo" { } ` -const testAccVpnGatewayConfigDetach = ` -resource "aws_vpc" "foo" { - cidr_block = "10.1.0.0/16" -} - -resource "aws_vpn_gateway" "foo" { - vpc_id = "" -} -` - const testAccCheckVpnGatewayConfigTags = ` resource "aws_vpc" "foo" { cidr_block = "10.1.0.0/16" @@ -305,3 +323,39 @@ resource "aws_vpn_gateway" "foo" { } } ` + +const testAccCheckVpnGatewayConfigReattach = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_vpc" "bar" { + cidr_block = "10.2.0.0/16" +} + +resource "aws_vpn_gateway" "foo" { + vpc_id = "${aws_vpc.foo.id}" +} + +resource "aws_vpn_gateway" "bar" { + vpc_id = "${aws_vpc.bar.id}" +} +` + +const testAccCheckVpnGatewayConfigReattachChange = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_vpc" "bar" { + cidr_block = "10.2.0.0/16" +} + +resource "aws_vpn_gateway" "foo" { + vpc_id = "${aws_vpc.bar.id}" +} + +resource "aws_vpn_gateway" "bar" { + vpc_id = "${aws_vpc.foo.id}" +} +` diff --git a/builtin/providers/template/datasource_template_file_test.go b/builtin/providers/template/datasource_template_file_test.go index 7b13f69e6e..43dda582ca 100644 --- a/builtin/providers/template/datasource_template_file_test.go +++ b/builtin/providers/template/datasource_template_file_test.go @@ -122,8 +122,8 @@ func TestValidateVarsAttribute(t *testing.T) { func TestTemplateSharedMemoryRace(t *testing.T) { var wg sync.WaitGroup for i := 0; i < 100; i++ { - go func(wg *sync.WaitGroup, t *testing.T, i int) { - wg.Add(1) + wg.Add(1) + go func(t *testing.T, i int) { out, err := execute("don't panic!", map[string]interface{}{}) if err != nil { t.Fatalf("err: %s", err) @@ -132,7 +132,7 @@ func TestTemplateSharedMemoryRace(t *testing.T) { t.Fatalf("bad output: %s", out) } wg.Done() - }(&wg, t, i) + }(t, i) } wg.Wait() } diff --git a/builtin/providers/vsphere/resource_vsphere_file.go b/builtin/providers/vsphere/resource_vsphere_file.go index 55d3d6cbb8..c8afe05d92 100644 --- a/builtin/providers/vsphere/resource_vsphere_file.go +++ b/builtin/providers/vsphere/resource_vsphere_file.go @@ -3,6 +3,7 @@ package vsphere import ( "fmt" "log" + "strings" "github.com/hashicorp/terraform/helper/schema" "github.com/vmware/govmomi" @@ -13,10 +14,14 @@ import ( ) type file struct { - datacenter string - datastore string - sourceFile string - destinationFile string + sourceDatacenter string + datacenter string + sourceDatastore string + datastore string + sourceFile string + destinationFile string + createDirectories bool + copyFile bool } func resourceVSphereFile() *schema.Resource { @@ -30,10 +35,20 @@ func resourceVSphereFile() *schema.Resource { "datacenter": { Type: schema.TypeString, Optional: true, + }, + + "source_datacenter": { + Type: schema.TypeString, + Optional: true, ForceNew: true, }, "datastore": { + Type: schema.TypeString, + Required: true, + }, + + "source_datastore": { Type: schema.TypeString, Optional: true, ForceNew: true, @@ -49,6 +64,11 @@ func resourceVSphereFile() *schema.Resource { Type: schema.TypeString, Required: true, }, + + "create_directories": { + Type: schema.TypeBool, + Optional: true, + }, }, } } @@ -60,10 +80,20 @@ func resourceVSphereFileCreate(d *schema.ResourceData, meta interface{}) error { f := file{} + if v, ok := d.GetOk("source_datacenter"); ok { + f.sourceDatacenter = v.(string) + f.copyFile = true + } + if v, ok := d.GetOk("datacenter"); ok { f.datacenter = v.(string) } + if v, ok := d.GetOk("source_datastore"); ok { + f.sourceDatastore = v.(string) + f.copyFile = true + } + if v, ok := d.GetOk("datastore"); ok { f.datastore = v.(string) } else { @@ -82,6 +112,10 @@ func resourceVSphereFileCreate(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("destination_file argument is required") } + if v, ok := d.GetOk("create_directories"); ok { + f.createDirectories = v.(bool) + } + err := createFile(client, &f) if err != nil { return err @@ -108,16 +142,53 @@ func createFile(client *govmomi.Client, f *file) error { return fmt.Errorf("error %s", err) } - dsurl, err := ds.URL(context.TODO(), dc, f.destinationFile) - if err != nil { - return err + if f.copyFile { + // Copying file from withing vSphere + source_dc, err := finder.Datacenter(context.TODO(), f.sourceDatacenter) + if err != nil { + return fmt.Errorf("error %s", err) + } + finder = finder.SetDatacenter(dc) + + source_ds, err := getDatastore(finder, f.sourceDatastore) + if err != nil { + return fmt.Errorf("error %s", err) + } + + fm := object.NewFileManager(client.Client) + if f.createDirectories { + directoryPathIndex := strings.LastIndex(f.destinationFile, "/") + path := f.destinationFile[0:directoryPathIndex] + err = fm.MakeDirectory(context.TODO(), ds.Path(path), dc, true) + if err != nil { + return fmt.Errorf("error %s", err) + } + } + task, err := fm.CopyDatastoreFile(context.TODO(), source_ds.Path(f.sourceFile), source_dc, ds.Path(f.destinationFile), dc, true) + + if err != nil { + return fmt.Errorf("error %s", err) + } + + _, err = task.WaitForResult(context.TODO(), nil) + if err != nil { + return fmt.Errorf("error %s", err) + } + + } else { + // Uploading file to vSphere + dsurl, err := ds.URL(context.TODO(), dc, f.destinationFile) + if err != nil { + return fmt.Errorf("error %s", err) + } + + p := soap.DefaultUpload + err = client.Client.UploadFile(f.sourceFile, dsurl, &p) + if err != nil { + return fmt.Errorf("error %s", err) + } } - p := soap.DefaultUpload - err = client.Client.UploadFile(f.sourceFile, dsurl, &p) - if err != nil { - return fmt.Errorf("error %s", err) - } return nil } @@ -126,10 +197,18 @@ func resourceVSphereFileRead(d *schema.ResourceData, meta interface{}) error { log.Printf("[DEBUG] reading file: %#v", d) f := file{} + if v, ok := d.GetOk("source_datacenter"); ok { + f.sourceDatacenter = v.(string) + } + if v, ok := d.GetOk("datacenter"); ok { f.datacenter = v.(string) } + if v, ok := d.GetOk("source_datastore"); ok { + f.sourceDatastore = v.(string) + } + if v, ok := d.GetOk("datastore"); ok { f.datastore = v.(string) } else { @@ -179,57 +258,69 @@ func resourceVSphereFileRead(d *schema.ResourceData, meta interface{}) error { func resourceVSphereFileUpdate(d *schema.ResourceData, meta interface{}) error { log.Printf("[DEBUG] updating file: %#v", d) - if d.HasChange("destination_file") { - oldDestinationFile, newDestinationFile := d.GetChange("destination_file") - f := file{} - if v, ok := d.GetOk("datacenter"); ok { - f.datacenter = v.(string) - } - - if v, ok := d.GetOk("datastore"); ok { - f.datastore = v.(string) + if d.HasChange("destination_file") || d.HasChange("datacenter") || d.HasChange("datastore") { + // File needs to be moved, get old and new destination changes + var oldDataceneter, newDatacenter, oldDatastore, newDatastore, oldDestinationFile, newDestinationFile string + if d.HasChange("datacenter") { + tmpOldDataceneter, tmpNewDatacenter := d.GetChange("datacenter") + oldDataceneter = tmpOldDataceneter.(string) + newDatacenter = tmpNewDatacenter.(string) } else { - return fmt.Errorf("datastore argument is required") + if v, ok := d.GetOk("datacenter"); ok { + oldDataceneter = v.(string) + newDatacenter = oldDataceneter + } } - - if v, ok := d.GetOk("source_file"); ok { - f.sourceFile = v.(string) + if d.HasChange("datastore") { + tmpOldDatastore, tmpNewDatastore := d.GetChange("datastore") + oldDatastore = tmpOldDatastore.(string) + newDatastore = tmpNewDatastore.(string) } else { - return fmt.Errorf("source_file argument is required") + oldDatastore = d.Get("datastore").(string) + newDatastore = oldDatastore } - - if v, ok := d.GetOk("destination_file"); ok { - f.destinationFile = v.(string) + if d.HasChange("destination_file") { + tmpOldDestinationFile, tmpNewDestinationFile := d.GetChange("destination_file") + oldDestinationFile = tmpOldDestinationFile.(string) + newDestinationFile = tmpNewDestinationFile.(string) } else { - return fmt.Errorf("destination_file argument is required") + oldDestinationFile = d.Get("destination_file").(string) + newDestinationFile = oldDestinationFile } + // Get old and new dataceter and datastore client := meta.(*govmomi.Client) - dc, err := getDatacenter(client, f.datacenter) + dcOld, err := getDatacenter(client, oldDataceneter) + if err != nil { + return err + } + dcNew, err := getDatacenter(client, newDatacenter) if err != nil { return err } - finder := find.NewFinder(client.Client, true) - finder = finder.SetDatacenter(dc) - - ds, err := getDatastore(finder, f.datastore) + finder = finder.SetDatacenter(dcOld) + dsOld, err := getDatastore(finder, oldDatastore) + if err != nil { + return fmt.Errorf("error %s", err) + } + finder = finder.SetDatacenter(dcNew) + dsNew, err := getDatastore(finder, newDatastore) if err != nil { return fmt.Errorf("error %s", err) } + // Move file between old/new dataceter, datastore and path (destination_file) fm := object.NewFileManager(client.Client) - task, err := fm.MoveDatastoreFile(context.TODO(), ds.Path(oldDestinationFile.(string)), dc, ds.Path(newDestinationFile.(string)), dc, true) + task, err := fm.MoveDatastoreFile(context.TODO(), dsOld.Path(oldDestinationFile), dcOld, dsNew.Path(newDestinationFile), dcNew, true) if err != nil { return err } - _, err = task.WaitForResult(context.TODO(), nil) if err != nil { return err } - } return nil diff --git a/builtin/providers/vsphere/resource_vsphere_file_test.go b/builtin/providers/vsphere/resource_vsphere_file_test.go index 81520b0cb4..7e5aa44e73 100644 --- a/builtin/providers/vsphere/resource_vsphere_file_test.go +++ b/builtin/providers/vsphere/resource_vsphere_file_test.go @@ -14,7 +14,7 @@ import ( "golang.org/x/net/context" ) -// Basic file creation +// Basic file creation (upload to vSphere) func TestAccVSphereFile_basic(t *testing.T) { testVmdkFileData := []byte("# Disk DescriptorFile\n") testVmdkFile := "/tmp/tf_test.vmdk" @@ -55,6 +55,59 @@ func TestAccVSphereFile_basic(t *testing.T) { os.Remove(testVmdkFile) } +// Basic file copy within vSphere +func TestAccVSphereFile_basicUploadAndCopy(t *testing.T) { + testVmdkFileData := []byte("# Disk DescriptorFile\n") + sourceFile := "/tmp/tf_test.vmdk" + uploadResourceName := "myfileupload" + copyResourceName := "myfilecopy" + sourceDatacenter := os.Getenv("VSPHERE_DATACENTER") + datacenter := sourceDatacenter + sourceDatastore := os.Getenv("VSPHERE_DATASTORE") + datastore := sourceDatastore + destinationFile := "tf_file_test.vmdk" + sourceFileCopy := "${vsphere_file." + uploadResourceName + ".destination_file}" + destinationFileCopy := "tf_file_test_copy.vmdk" + + err := ioutil.WriteFile(sourceFile, testVmdkFileData, 0644) + if err != nil { + t.Errorf("error %s", err) + return + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVSphereFileDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf( + testAccCheckVSphereFileCopyConfig, + uploadResourceName, + datacenter, + datastore, + sourceFile, + destinationFile, + copyResourceName, + datacenter, + datacenter, + datastore, + datastore, + sourceFileCopy, + destinationFileCopy, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVSphereFileExists("vsphere_file."+uploadResourceName, destinationFile, true), + testAccCheckVSphereFileExists("vsphere_file."+copyResourceName, destinationFileCopy, true), + resource.TestCheckResourceAttr("vsphere_file."+uploadResourceName, "destination_file", destinationFile), + resource.TestCheckResourceAttr("vsphere_file."+copyResourceName, "destination_file", destinationFileCopy), + ), + }, + }, + }) + os.Remove(sourceFile) +} + // file creation followed by a rename of file (update) func TestAccVSphereFile_renamePostCreation(t *testing.T) { testVmdkFileData := []byte("# Disk DescriptorFile\n") @@ -67,7 +120,7 @@ func TestAccVSphereFile_renamePostCreation(t *testing.T) { datacenter := os.Getenv("VSPHERE_DATACENTER") datastore := os.Getenv("VSPHERE_DATASTORE") - testMethod := "basic" + testMethod := "create_upgrade" resourceName := "vsphere_file." + testMethod destinationFile := "tf_test_file.vmdk" destinationFileMoved := "tf_test_file_moved.vmdk" @@ -76,7 +129,7 @@ func TestAccVSphereFile_renamePostCreation(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, - CheckDestroy: testAccCheckVSphereFolderDestroy, + CheckDestroy: testAccCheckVSphereFileDestroy, Steps: []resource.TestStep{ { Config: fmt.Sprintf( @@ -113,6 +166,84 @@ func TestAccVSphereFile_renamePostCreation(t *testing.T) { os.Remove(testVmdkFile) } +// file upload, then copy, finally the copy is renamed (moved) (update) +func TestAccVSphereFile_uploadAndCopyAndUpdate(t *testing.T) { + testVmdkFileData := []byte("# Disk DescriptorFile\n") + sourceFile := "/tmp/tf_test.vmdk" + uploadResourceName := "myfileupload" + copyResourceName := "myfilecopy" + sourceDatacenter := os.Getenv("VSPHERE_DATACENTER") + datacenter := sourceDatacenter + sourceDatastore := os.Getenv("VSPHERE_DATASTORE") + datastore := sourceDatastore + destinationFile := "tf_file_test.vmdk" + sourceFileCopy := "${vsphere_file." + uploadResourceName + ".destination_file}" + destinationFileCopy := "tf_file_test_copy.vmdk" + destinationFileMoved := "tf_test_file_moved.vmdk" + + err := ioutil.WriteFile(sourceFile, testVmdkFileData, 0644) + if err != nil { + t.Errorf("error %s", err) + return + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVSphereFileDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf( + testAccCheckVSphereFileCopyConfig, + uploadResourceName, + datacenter, + datastore, + sourceFile, + destinationFile, + copyResourceName, + datacenter, + datacenter, + datastore, + datastore, + sourceFileCopy, + destinationFileCopy, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVSphereFileExists("vsphere_file."+uploadResourceName, destinationFile, true), + testAccCheckVSphereFileExists("vsphere_file."+copyResourceName, destinationFileCopy, true), + resource.TestCheckResourceAttr("vsphere_file."+uploadResourceName, "destination_file", destinationFile), + resource.TestCheckResourceAttr("vsphere_file."+copyResourceName, "destination_file", destinationFileCopy), + ), + }, + { + Config: fmt.Sprintf( + testAccCheckVSphereFileCopyConfig, + uploadResourceName, + datacenter, + datastore, + sourceFile, + destinationFile, + copyResourceName, + datacenter, + datacenter, + datastore, + datastore, + sourceFileCopy, + destinationFileMoved, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVSphereFileExists("vsphere_file."+uploadResourceName, destinationFile, true), + testAccCheckVSphereFileExists("vsphere_file."+copyResourceName, destinationFileCopy, false), + testAccCheckVSphereFileExists("vsphere_file."+copyResourceName, destinationFileMoved, true), + resource.TestCheckResourceAttr("vsphere_file."+uploadResourceName, "destination_file", destinationFile), + resource.TestCheckResourceAttr("vsphere_file."+copyResourceName, "destination_file", destinationFileMoved), + ), + }, + }, + }) + os.Remove(sourceFile) +} + func testAccCheckVSphereFileDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*govmomi.Client) finder := find.NewFinder(client.Client, true) @@ -201,3 +332,19 @@ resource "vsphere_file" "%s" { destination_file = "%s" } ` +const testAccCheckVSphereFileCopyConfig = ` +resource "vsphere_file" "%s" { + datacenter = "%s" + datastore = "%s" + source_file = "%s" + destination_file = "%s" +} +resource "vsphere_file" "%s" { + source_datacenter = "%s" + datacenter = "%s" + source_datastore = "%s" + datastore = "%s" + source_file = "%s" + destination_file = "%s" +} +` diff --git a/command/push.go b/command/push.go index 0a62905949..d9dda82b7b 100644 --- a/command/push.go +++ b/command/push.go @@ -47,6 +47,22 @@ func (c *PushCommand) Run(args []string) int { overwriteMap[v] = struct{}{} } + // This is a map of variables specifically from the CLI that we want to overwrite. + // We need this because there is a chance that the user is trying to modify + // a variable we don't see in our context, but which exists in this atlas + // environment. + cliVars := make(map[string]string) + for k, v := range c.variables { + if _, ok := overwriteMap[k]; ok { + if val, ok := v.(string); ok { + cliVars[k] = val + } else { + c.Ui.Error(fmt.Sprintf("Error reading value for variable: %s", k)) + return 1 + } + } + } + // The pwd is used for the configuration path if one is not given pwd, err := os.Getwd() if err != nil { @@ -145,19 +161,14 @@ func (c *PushCommand) Run(args []string) int { return 1 } - // filter any overwrites from the atlas vars - for k := range overwriteMap { - delete(atlasVars, k) - } - // Set remote variables in the context if we don't have a value here. These // don't have to be correct, it just prevents the Input walk from prompting - // the user for input, The atlas variable may be an hcl-encoded object, but - // we're just going to set it as the raw string value. + // the user for input. ctxVars := ctx.Variables() - for k, av := range atlasVars { + atlasVarSentry := "ATLAS_78AC153CA649EAA44815DAD6CBD4816D" + for k, _ := range atlasVars { if _, ok := ctxVars[k]; !ok { - ctx.SetVariable(k, av.Value) + ctx.SetVariable(k, atlasVarSentry) } } @@ -203,23 +214,47 @@ func (c *PushCommand) Run(args []string) int { return 1 } - // Output to the user the variables that will be uploaded + // List of the vars we're uploading to display to the user. + // We always upload all vars from atlas, but only report them if they are overwritten. var setVars []string + // variables to upload var uploadVars []atlas.TFVar - // Now we can combine the vars for upload to atlas and list the variables - // we're uploading for the user + // first add all the variables we want to send which have been serialized + // from the local context. for _, sv := range serializedVars { - if av, ok := atlasVars[sv.Key]; ok { - // this belongs to Atlas - uploadVars = append(uploadVars, av) - } else { - // we're uploading our local version - setVars = append(setVars, sv.Key) + _, inOverwrite := overwriteMap[sv.Key] + _, inAtlas := atlasVars[sv.Key] + + // We have a variable that's not in atlas, so always send it. + if !inAtlas { uploadVars = append(uploadVars, sv) + setVars = append(setVars, sv.Key) } + // We're overwriting an atlas variable. + // We also want to check that we + // don't send the dummy sentry value back to atlas. This could happen + // if it's specified as an overwrite on the cli, but we didn't set a + // new value. + if inAtlas && inOverwrite && sv.Value != atlasVarSentry { + uploadVars = append(uploadVars, sv) + setVars = append(setVars, sv.Key) + + // remove this value from the atlas vars, because we're going to + // send back the remainder regardless. + delete(atlasVars, sv.Key) + } + } + + // now send back all the existing atlas vars, inserting any overwrites from the cli. + for k, av := range atlasVars { + if v, ok := cliVars[k]; ok { + av.Value = v + setVars = append(setVars, k) + } + uploadVars = append(uploadVars, av) } sort.Strings(setVars) diff --git a/command/push_test.go b/command/push_test.go index 8c6cfa923f..2f78dd6fa0 100644 --- a/command/push_test.go +++ b/command/push_test.go @@ -264,6 +264,97 @@ func TestPush_localOverride(t *testing.T) { } } +// This tests that the push command will override Atlas variables +// even if we don't have it defined locally +func TestPush_remoteOverride(t *testing.T) { + // Disable test mode so input would be asked and setup the + // input reader/writers. + test = false + defer func() { test = true }() + defaultInputReader = bytes.NewBufferString("nope\n") + defaultInputWriter = new(bytes.Buffer) + + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + // Create remote state file, this should be pulled + conf, srv := testRemoteState(t, testState(), 200) + defer srv.Close() + + // Persist local remote state + s := terraform.NewState() + s.Serial = 5 + s.Remote = conf + testStateFileRemote(t, s) + + // Path where the archive will be "uploaded" to + archivePath := testTempFile(t) + defer os.Remove(archivePath) + + client := &mockPushClient{File: archivePath} + // Provided vars should override existing ones + client.GetResult = map[string]atlas.TFVar{ + "remote": atlas.TFVar{ + Key: "remote", + Value: "old", + }, + } + ui := new(cli.MockUi) + c := &PushCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + + client: client, + } + + path := testFixturePath("push-tfvars") + args := []string{ + "-var-file", path + "/terraform.tfvars", + "-vcs=false", + "-overwrite=remote", + "-var", + "remote=new", + path, + } + + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + actual := testArchiveStr(t, archivePath) + expected := []string{ + ".terraform/", + ".terraform/terraform.tfstate", + "main.tf", + "terraform.tfvars", + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } + + if client.UpsertOptions.Name != "foo" { + t.Fatalf("bad: %#v", client.UpsertOptions) + } + + found := false + // find the "remote" var and make sure we're going to set it + for _, tfVar := range client.UpsertOptions.TFVars { + if tfVar.Key == "remote" { + found = true + if tfVar.Value != "new" { + t.Log("'remote' variable should be set to 'new'") + t.Fatalf("sending instead: %#v", tfVar) + } + } + } + + if !found { + t.Fatal("'remote' variable not being sent to atlas") + } +} + // This tests that the push command prefers Atlas variables over // local ones. func TestPush_preferAtlas(t *testing.T) { diff --git a/state/remote/atlas.go b/state/remote/atlas.go index 24e81f177c..5343c0236d 100644 --- a/state/remote/atlas.go +++ b/state/remote/atlas.go @@ -23,6 +23,7 @@ import ( const ( // defaultAtlasServer is used when no address is given defaultAtlasServer = "https://atlas.hashicorp.com/" + atlasTokenHeader = "X-Atlas-Token" ) func atlasFactory(conf map[string]string) (Client, error) { @@ -92,6 +93,8 @@ func (c *AtlasClient) Get() (*Payload, error) { return nil, fmt.Errorf("Failed to make HTTP request: %v", err) } + req.Header.Set(atlasTokenHeader, c.AccessToken) + // Request the url client, err := c.http() if err != nil { @@ -170,6 +173,7 @@ func (c *AtlasClient) Put(state []byte) error { } // Prepare the request + req.Header.Set(atlasTokenHeader, c.AccessToken) req.Header.Set("Content-MD5", b64) req.Header.Set("Content-Type", "application/json") req.ContentLength = int64(len(state)) @@ -204,6 +208,7 @@ func (c *AtlasClient) Delete() error { if err != nil { return fmt.Errorf("Failed to make HTTP request: %v", err) } + req.Header.Set(atlasTokenHeader, c.AccessToken) // Make the request client, err := c.http() @@ -249,7 +254,6 @@ func (c *AtlasClient) url() *url.URL { values := url.Values{} values.Add("atlas_run_id", c.RunId) - values.Add("access_token", c.AccessToken) return &url.URL{ Scheme: c.ServerURL.Scheme, diff --git a/state/remote/atlas_test.go b/state/remote/atlas_test.go index 1d73540a40..9d4f226fef 100644 --- a/state/remote/atlas_test.go +++ b/state/remote/atlas_test.go @@ -218,6 +218,17 @@ func (f *fakeAtlas) NoConflictAllowed(b bool) { } func (f *fakeAtlas) handler(resp http.ResponseWriter, req *http.Request) { + // access tokens should only be sent as a header + if req.FormValue("access_token") != "" { + http.Error(resp, "access_token in request params", http.StatusBadRequest) + return + } + + if req.Header.Get(atlasTokenHeader) == "" { + http.Error(resp, "missing access token", http.StatusBadRequest) + return + } + switch req.Method { case "GET": // Respond with the current stored state. diff --git a/website/source/docs/providers/aws/r/elasticsearch_domain.html.markdown b/website/source/docs/providers/aws/r/elasticsearch_domain.html.markdown index dac78a87c3..6dd083f0fd 100644 --- a/website/source/docs/providers/aws/r/elasticsearch_domain.html.markdown +++ b/website/source/docs/providers/aws/r/elasticsearch_domain.html.markdown @@ -14,6 +14,7 @@ description: |- ``` resource "aws_elasticsearch_domain" "es" { domain_name = "tf-test" + elasticsearch_version = "1.5" advanced_options { "rest.action.multi.allow_explicit_index" = true } @@ -54,6 +55,7 @@ The following arguments are supported: * `ebs_options` - (Optional) EBS related options, see below. * `cluster_config` - (Optional) Cluster configuration of the domain, see below. * `snapshot_options` - (Optional) Snapshot related options, see below. +* `elasticsearch_version` - (Optional) The version of ElasticSearch to deploy. Only valid values are `1.5` and `2.3`. Defaults to `1.5` * `tags` - (Optional) A mapping of tags to assign to the resource **ebs_options** supports the following attributes: diff --git a/website/source/docs/providers/aws/r/vpn_gateway_attachment.html.markdown b/website/source/docs/providers/aws/r/vpn_gateway_attachment.html.markdown new file mode 100644 index 0000000000..8099128315 --- /dev/null +++ b/website/source/docs/providers/aws/r/vpn_gateway_attachment.html.markdown @@ -0,0 +1,57 @@ +--- +layout: "aws" +page_title: "AWS: aws_vpn_gateway_attachment" +sidebar_current: "docs-aws-resource-vpn-gateway-attachment" +description: |- + Provides a Virtual Private Gateway attachment resource. +--- + +# aws\_vpn\_gateway\_attachment + +Provides a Virtual Private Gateway attachment resource, allowing for an existing +hardware VPN gateway to be attached and/or detached from a VPC. + +-> **Note:** The [`aws_vpn_gateway`](vpn_gateway.html) +resource can also automatically attach the Virtual Private Gateway it creates +to an existing VPC by setting the [`vpc_id`](vpn_gateway.html#vpc_id) attribute accordingly. + +## Example Usage + +``` +resource "aws_vpc" "network" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_vpn_gateway" "vpn" { + tags { + Name = "example-vpn-gateway" + } +} + +resource "aws_vpn_gateway_attachment" "vpn_attachment" { + vpc_id = "${aws_vpc.network.id}" + vpn_gateway_id = "${aws_vpn_gateway.vpn.id}" +} +``` + +See [Virtual Private Cloud](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Introduction.html) +and [Virtual Private Gateway](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_VPN.html) user +guides for more information. + +## Argument Reference + +The following arguments are supported: + +* `vpc_id` - (Required) The ID of the VPC. +* `vpn_gateway_id` - (Required) The ID of the Virtual Private Gateway. + +## Attributes Reference + +The following attributes are exported: + +* `vpc_id` - The ID of the VPC that Virtual Private Gateway is attached to. +* `vpn_gateway_id` - The ID of the Virtual Private Gateway. + +## Import + +This resource does not support importing. diff --git a/website/source/docs/providers/vsphere/r/file.html.markdown b/website/source/docs/providers/vsphere/r/file.html.markdown index 443aa3046a..9bd4c4b17c 100644 --- a/website/source/docs/providers/vsphere/r/file.html.markdown +++ b/website/source/docs/providers/vsphere/r/file.html.markdown @@ -3,28 +3,49 @@ layout: "vsphere" page_title: "VMware vSphere: vsphere_file" sidebar_current: "docs-vsphere-resource-file" description: |- - Provides a VMware vSphere virtual machine file resource. This can be used to upload files (e.g. vmdk disks) from the Terraform host machine to a remote vSphere. + Provides a VMware vSphere virtual machine file resource. This can be used to upload files (e.g. vmdk disks) from the Terraform host machine to a remote vSphere or copy fields withing vSphere. --- # vsphere\_file -Provides a VMware vSphere virtual machine file resource. This can be used to upload files (e.g. vmdk disks) from the Terraform host machine to a remote vSphere. +Provides a VMware vSphere virtual machine file resource. This can be used to upload files (e.g. vmdk disks) from the Terraform host machine to a remote vSphere. The file resource can also be used to copy files within vSphere. Files can be copied between Datacenters and/or Datastores. -## Example Usage +Updates to file resources will handle moving a file to a new destination (datacenter and/or datastore and/or destination_file). If any source parameter (e.g. `source_datastore`, `source_datacenter` or `source_file`) are changed, this results in a new resource (new file uploaded or copied and old one being deleted). +## Example Usages + +**Upload file to vSphere:** ``` -resource "vsphere_file" "ubuntu_disk" { +resource "vsphere_file" "ubuntu_disk_upload" { + datacenter = "my_datacenter" datastore = "local" source_file = "/home/ubuntu/my_disks/custom_ubuntu.vmdk" destination_file = "/my_path/disks/custom_ubuntu.vmdk" } ``` +**Copy file within vSphere:** +``` +resource "vsphere_file" "ubuntu_disk_copy" { + source_datacenter = "my_datacenter" + datacenter = "my_datacenter" + source_datastore = "local" + datastore = "local" + source_file = "/my_path/disks/custom_ubuntu.vmdk" + destination_file = "/my_path/custom_ubuntu_id.vmdk" +} +``` + ## Argument Reference +If `source_datacenter` and `source_datastore` are not provided, the file resource will upload the file from Terraform host. If either `source_datacenter` or `source_datastore` are provided, the file resource will copy from within specified locations in vSphere. + The following arguments are supported: -* `source_file` - (Required) The path to the file on the Terraform host that will be uploaded to vSphere. -* `destination_file` - (Required) The path to where the file should be uploaded to on vSphere. -* `datacenter` - (Optional) The name of a Datacenter in which the file will be created/uploaded to. -* `datastore` - (Required) The name of the Datastore in which to create/upload the file to. +* `source_file` - (Required) The path to the file being uploaded from the Terraform host to vSphere or copied within vSphere. +* `destination_file` - (Required) The path to where the file should be uploaded or copied to on vSphere. +* `source_datacenter` - (Optional) The name of a Datacenter in which the file will be copied from. +* `datacenter` - (Optional) The name of a Datacenter in which the file will be uploaded to. +* `source_datastore` - (Optional) The name of the Datastore in which file will be copied from. +* `datastore` - (Required) The name of the Datastore in which to upload the file to. +* `create_directories` - (Optional) Create directories in `destination_file` path parameter if any missing for copy operation. *Note: Directories are not deleted on destroy operation. \ No newline at end of file diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 8ba2ae80e6..4eec00ae69 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -885,6 +885,10 @@ aws_vpn_gateway + > + aws_vpn_gateway_attachment + +