diff --git a/CHANGELOG.md b/CHANGELOG.md index 7793d2a25b..e3d815db25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,21 +3,32 @@ BACKWARDS INCOMPATIBILITIES / NOTES: * provider/aws: Users of aws_cloudfront_distributions with custom_origins have been broken due to changes in the AWS API requiring `OriginReadTimeout` being set for updates. This has been fixed and will show as a change in terraform plan / apply. [GH-13367] +* provider/aws: Users of China and Gov clouds, cannot use the new tagging of volumes created as part of aws_instances [GH-14055] FEATURES: * **New Provider:** `gitlab` [GH-13898] +* **New Resource:** `heroku_app_feature` [GH-14035] IMPROVEMENTS: * provider/aws: Add support for CustomOrigin timeouts to aws_cloudfront_distribution [GH-13367] +* provider/azurerm: Expose the Private IP Address for a Load Balancer, if available [GH-13965] * provider/dnsimple: Add support for import for dnsimple_records [GH-9130] +* provider/nomad: Add TLS options [GH-13956] * provider/triton: Add support for reading provider configuration from `TRITON_*` environment variables in addition to `SDC_*`[GH-14000] +* provider/triton: Add `cloud_config` argument to `triton_machine` resources for Linux containers [GH-12840] +* provider/triton: Add `insecure_skip_tls_verify` [GH-14077] BUG FIXES: * provider/aws: Update aws_ebs_volume when attached [GH-14005] * provider/aws: Set aws_instance volume_tags to be Computed [GH-14007] +* provider/aws: Fix issue getting partition for federated users [GH-13992] +* provider/aws: aws_spot_instance_request not forcenew on volume_tags [GH-14046] +* provider/aws: Exclude aws_instance volume tagging for China and Gov Clouds [GH-14055] +* provider/digitalocean: Prevent diffs when using IDs of images instead of slugs [GH-13879] +* provider/google: ignore certain project services that can't be enabled directly via the api [GH-13730] * providers/heroku: Configure buildpacks correctly for both Org Apps and non-org Apps [GH-13990] ## 0.9.4 (26th April 2017) diff --git a/Makefile b/Makefile index 319492ef13..0eba369dc8 100644 --- a/Makefile +++ b/Makefile @@ -75,8 +75,8 @@ cover: # vet runs the Go source code static analysis tool `vet` to find # any common errors. vet: - @echo "go vet ." - @go vet $$(go list ./... | grep -v vendor/) ; if [ $$? -eq 1 ]; then \ + @echo 'go vet $$(go list ./... | grep -v /terraform/vendor/)' + @go vet $$(go list ./... | grep -v /terraform/vendor/) ; if [ $$? -eq 1 ]; then \ echo ""; \ echo "Vet found suspicious constructs. Please check the reported constructs"; \ echo "and fix them if necessary before submitting the code for review."; \ diff --git a/builtin/providers/aws/auth_helpers.go b/builtin/providers/aws/auth_helpers.go index 1a73c6e8b5..e808d4d390 100644 --- a/builtin/providers/aws/auth_helpers.go +++ b/builtin/providers/aws/auth_helpers.go @@ -54,7 +54,7 @@ func GetAccountInfo(iamconn *iam.IAM, stsconn *sts.STS, authProviderName string) awsErr, ok := err.(awserr.Error) // AccessDenied and ValidationError can be raised // if credentials belong to federated profile, so we ignore these - if !ok || (awsErr.Code() != "AccessDenied" && awsErr.Code() != "ValidationError") { + if !ok || (awsErr.Code() != "AccessDenied" && awsErr.Code() != "ValidationError" && awsErr.Code() != "InvalidClientTokenId") { return "", "", fmt.Errorf("Failed getting account ID via 'iam:GetUser': %s", err) } log.Printf("[DEBUG] Getting account ID via iam:GetUser failed: %s", err) diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 78fa93deb6..3270901309 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -171,6 +171,20 @@ func (c *AWSClient) DynamoDB() *dynamodb.DynamoDB { return c.dynamodbconn } +func (c *AWSClient) IsGovCloud() bool { + if c.region == "us-gov-west-1" { + return true + } + return false +} + +func (c *AWSClient) IsChinaCloud() bool { + if c.region == "cn-north-1" { + return true + } + return false +} + // Client configures and returns a fully initialized AWSClient func (c *Config) Client() (interface{}, error) { // Get the auth and region. This can fail if keys/regions were not diff --git a/builtin/providers/aws/resource_aws_instance.go b/builtin/providers/aws/resource_aws_instance.go index b648b3b35f..e6253b3bce 100644 --- a/builtin/providers/aws/resource_aws_instance.go +++ b/builtin/providers/aws/resource_aws_instance.go @@ -432,32 +432,35 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { runOpts.Ipv6Addresses = ipv6Addresses } - tagsSpec := make([]*ec2.TagSpecification, 0) + restricted := meta.(*AWSClient).IsGovCloud() || meta.(*AWSClient).IsChinaCloud() + if !restricted { + tagsSpec := make([]*ec2.TagSpecification, 0) - if v, ok := d.GetOk("tags"); ok { - tags := tagsFromMap(v.(map[string]interface{})) + if v, ok := d.GetOk("tags"); ok { + tags := tagsFromMap(v.(map[string]interface{})) - spec := &ec2.TagSpecification{ - ResourceType: aws.String("instance"), - Tags: tags, + spec := &ec2.TagSpecification{ + ResourceType: aws.String("instance"), + Tags: tags, + } + + tagsSpec = append(tagsSpec, spec) } - tagsSpec = append(tagsSpec, spec) - } + if v, ok := d.GetOk("volume_tags"); ok { + tags := tagsFromMap(v.(map[string]interface{})) - if v, ok := d.GetOk("volume_tags"); ok { - tags := tagsFromMap(v.(map[string]interface{})) + spec := &ec2.TagSpecification{ + ResourceType: aws.String("volume"), + Tags: tags, + } - spec := &ec2.TagSpecification{ - ResourceType: aws.String("volume"), - Tags: tags, + tagsSpec = append(tagsSpec, spec) } - tagsSpec = append(tagsSpec, spec) - } - - if len(tagsSpec) > 0 { - runOpts.TagSpecifications = tagsSpec + if len(tagsSpec) > 0 { + runOpts.TagSpecifications = tagsSpec + } } // Create the instance @@ -713,19 +716,24 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error { d.Partial(true) - if d.HasChange("tags") && !d.IsNewResource() { - if err := setTags(conn, d); err != nil { - return err - } else { - d.SetPartial("tags") + restricted := meta.(*AWSClient).IsGovCloud() || meta.(*AWSClient).IsChinaCloud() + + if d.HasChange("tags") { + if !d.IsNewResource() || !restricted { + if err := setTags(conn, d); err != nil { + return err + } else { + d.SetPartial("tags") + } } } - - if d.HasChange("volume_tags") && !d.IsNewResource() { - if err := setVolumeTags(conn, d); err != nil { - return err - } else { - d.SetPartial("volume_tags") + if d.HasChange("volume_tags") { + if !d.IsNewResource() || !restricted { + if err := setVolumeTags(conn, d); err != nil { + return err + } else { + d.SetPartial("volume_tags") + } } } diff --git a/builtin/providers/aws/resource_aws_kms_key.go b/builtin/providers/aws/resource_aws_kms_key.go index 2fa8e3287c..f95f76d95f 100644 --- a/builtin/providers/aws/resource_aws_kms_key.go +++ b/builtin/providers/aws/resource_aws_kms_key.go @@ -320,19 +320,33 @@ func updateKmsKeyStatus(conn *kms.KMS, id string, shouldBeEnabled bool) error { } func updateKmsKeyRotationStatus(conn *kms.KMS, d *schema.ResourceData) error { - var err error shouldEnableRotation := d.Get("enable_key_rotation").(bool) - if shouldEnableRotation { - log.Printf("[DEBUG] Enabling key rotation for KMS key %q", d.Id()) - _, err = conn.EnableKeyRotation(&kms.EnableKeyRotationInput{ - KeyId: aws.String(d.Id()), - }) - } else { - log.Printf("[DEBUG] Disabling key rotation for KMS key %q", d.Id()) - _, err = conn.DisableKeyRotation(&kms.DisableKeyRotationInput{ - KeyId: aws.String(d.Id()), - }) - } + + err := resource.Retry(5*time.Minute, func() *resource.RetryError { + var err error + if shouldEnableRotation { + log.Printf("[DEBUG] Enabling key rotation for KMS key %q", d.Id()) + _, err = conn.EnableKeyRotation(&kms.EnableKeyRotationInput{ + KeyId: aws.String(d.Id()), + }) + } else { + log.Printf("[DEBUG] Disabling key rotation for KMS key %q", d.Id()) + _, err = conn.DisableKeyRotation(&kms.DisableKeyRotationInput{ + KeyId: aws.String(d.Id()), + }) + } + + if err != nil { + awsErr, ok := err.(awserr.Error) + if ok && awsErr.Code() == "DisabledException" { + return resource.RetryableError(err) + } + + return resource.NonRetryableError(err) + } + + return nil + }) if err != nil { return fmt.Errorf("Failed to set key rotation for %q to %t: %q", diff --git a/builtin/providers/aws/resource_aws_spot_instance_request.go b/builtin/providers/aws/resource_aws_spot_instance_request.go index c0e87e5546..1de3b71211 100644 --- a/builtin/providers/aws/resource_aws_spot_instance_request.go +++ b/builtin/providers/aws/resource_aws_spot_instance_request.go @@ -25,7 +25,7 @@ func resourceAwsSpotInstanceRequest() *schema.Resource { // Everything on a spot instance is ForceNew except tags for k, v := range s { - if k == "tags" { + if k == "tags" || k == "volume_tags" { continue } v.ForceNew = true diff --git a/builtin/providers/azurerm/resource_arm_loadbalancer.go b/builtin/providers/azurerm/resource_arm_loadbalancer.go index 60bb95990a..940b3fb981 100644 --- a/builtin/providers/azurerm/resource_arm_loadbalancer.go +++ b/builtin/providers/azurerm/resource_arm_loadbalancer.go @@ -92,6 +92,11 @@ func resourceArmLoadBalancer() *schema.Resource { }, }, + "private_ip_address": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchema(), }, } @@ -172,7 +177,17 @@ func resourecArmLoadBalancerRead(d *schema.ResourceData, meta interface{}) error d.Set("resource_group_name", id.ResourceGroup) if loadBalancer.LoadBalancerPropertiesFormat != nil && loadBalancer.LoadBalancerPropertiesFormat.FrontendIPConfigurations != nil { - d.Set("frontend_ip_configuration", flattenLoadBalancerFrontendIpConfiguration(loadBalancer.LoadBalancerPropertiesFormat.FrontendIPConfigurations)) + ipconfigs := loadBalancer.LoadBalancerPropertiesFormat.FrontendIPConfigurations + d.Set("frontend_ip_configuration", flattenLoadBalancerFrontendIpConfiguration(ipconfigs)) + + for _, config := range *ipconfigs { + if config.FrontendIPConfigurationPropertiesFormat.PrivateIPAddress != nil { + d.Set("private_ip_address", config.FrontendIPConfigurationPropertiesFormat.PrivateIPAddress) + + // set the private IP address at most once + break + } + } } flattenAndSetTags(d, loadBalancer.Tags) diff --git a/builtin/providers/digitalocean/config.go b/builtin/providers/digitalocean/config.go index a0a9115ae6..b32be5938f 100644 --- a/builtin/providers/digitalocean/config.go +++ b/builtin/providers/digitalocean/config.go @@ -2,9 +2,12 @@ package digitalocean import ( "log" + "net/http" + "net/http/httputil" "time" "github.com/digitalocean/godo" + "github.com/hashicorp/terraform/helper/logging" "github.com/hashicorp/terraform/helper/resource" "golang.org/x/oauth2" ) @@ -21,11 +24,31 @@ func (c *Config) Client() (*godo.Client, error) { client := godo.NewClient(oauth2.NewClient(oauth2.NoContext, tokenSrc)) + if logging.IsDebugOrHigher() { + client.OnRequestCompleted(logRequestAndResponse) + } + log.Printf("[INFO] DigitalOcean Client configured for URL: %s", client.BaseURL.String()) return client, nil } +func logRequestAndResponse(req *http.Request, resp *http.Response) { + reqData, err := httputil.DumpRequest(req, true) + if err == nil { + log.Printf("[DEBUG] "+logReqMsg, string(reqData)) + } else { + log.Printf("[ERROR] DigitalOcean API Request error: %#v", err) + } + + respData, err := httputil.DumpResponse(resp, true) + if err == nil { + log.Printf("[DEBUG] "+logRespMsg, string(respData)) + } else { + log.Printf("[ERROR] DigitalOcean API Response error: %#v", err) + } +} + // waitForAction waits for the action to finish using the resource.StateChangeConf. func waitForAction(client *godo.Client, action *godo.Action) error { var ( @@ -61,3 +84,13 @@ func waitForAction(client *godo.Client, action *godo.Action) error { }).WaitForState() return err } + +const logReqMsg = `DigitalOcean API Request Details: +---[ REQUEST ]--------------------------------------- +%s +-----------------------------------------------------` + +const logRespMsg = `DigitalOcean API Response Details: +---[ RESPONSE ]-------------------------------------- +%s +-----------------------------------------------------` diff --git a/builtin/providers/digitalocean/datasource_digitaloceal_image.go b/builtin/providers/digitalocean/datasource_digitalocean_image.go similarity index 100% rename from builtin/providers/digitalocean/datasource_digitaloceal_image.go rename to builtin/providers/digitalocean/datasource_digitalocean_image.go diff --git a/builtin/providers/digitalocean/datasource_digitaloceal_image_test.go b/builtin/providers/digitalocean/datasource_digitalocean_image_test.go similarity index 100% rename from builtin/providers/digitalocean/datasource_digitaloceal_image_test.go rename to builtin/providers/digitalocean/datasource_digitalocean_image_test.go diff --git a/builtin/providers/digitalocean/resource_digitalocean_droplet.go b/builtin/providers/digitalocean/resource_digitalocean_droplet.go index 48372d0e75..fdae4dd034 100644 --- a/builtin/providers/digitalocean/resource_digitalocean_droplet.go +++ b/builtin/providers/digitalocean/resource_digitalocean_droplet.go @@ -260,10 +260,13 @@ func resourceDigitalOceanDropletRead(d *schema.ResourceData, meta interface{}) e return fmt.Errorf("Error retrieving droplet: %s", err) } - if droplet.Image.Slug != "" { - d.Set("image", droplet.Image.Slug) - } else { + _, err = strconv.Atoi(d.Get("image").(string)) + if err == nil || droplet.Image.Slug == "" { + // The image field is provided as an ID (number), or + // the image bash no slug. In both cases we store it as an ID. d.Set("image", droplet.Image.ID) + } else { + d.Set("image", droplet.Image.Slug) } d.Set("name", droplet.Name) diff --git a/builtin/providers/digitalocean/resource_digitalocean_droplet_test.go b/builtin/providers/digitalocean/resource_digitalocean_droplet_test.go index 3f813c953d..1d6f780428 100644 --- a/builtin/providers/digitalocean/resource_digitalocean_droplet_test.go +++ b/builtin/providers/digitalocean/resource_digitalocean_droplet_test.go @@ -41,16 +41,31 @@ func TestAccDigitalOceanDroplet_Basic(t *testing.T) { resource.TestCheckResourceAttr( "digitalocean_droplet.foobar", "user_data", "foobar"), ), - Destroy: false, - }, - { - Config: testAccCheckDigitalOceanDropletConfig_basic(rInt), - PlanOnly: true, }, }, }) } +func TestAccDigitalOceanDroplet_WithID(t *testing.T) { + var droplet godo.Droplet + rInt := acctest.RandInt() + // TODO: not hardcode this as it will change over time + centosID := 22995941 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanDropletDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckDigitalOceanDropletConfig_withID(centosID, rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanDropletExists("digitalocean_droplet.foobar", &droplet), + ), + }, + }, + }) +} func TestAccDigitalOceanDroplet_withSSH(t *testing.T) { var droplet godo.Droplet rInt := acctest.RandInt() @@ -504,6 +519,17 @@ resource "digitalocean_droplet" "foobar" { }`, rInt) } +func testAccCheckDigitalOceanDropletConfig_withID(imageID, rInt int) string { + return fmt.Sprintf(` +resource "digitalocean_droplet" "foobar" { + name = "foo-%d" + size = "512mb" + image = "%d" + region = "nyc3" + user_data = "foobar" +}`, rInt, imageID) +} + func testAccCheckDigitalOceanDropletConfig_withSSH(rInt int) string { return fmt.Sprintf(` resource "digitalocean_ssh_key" "foobar" { diff --git a/builtin/providers/google/data_source_google_compute_network_test.go b/builtin/providers/google/data_source_google_compute_network_test.go index bbf70af673..fe0aac8fae 100644 --- a/builtin/providers/google/data_source_google_compute_network_test.go +++ b/builtin/providers/google/data_source_google_compute_network_test.go @@ -2,18 +2,21 @@ package google import ( "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "testing" ) func TestAccDataSourceGoogleNetwork(t *testing.T) { + networkName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ resource.TestStep{ - Config: TestAccDataSourceGoogleNetworkConfig, + Config: testAccDataSourceGoogleNetworkConfig(networkName), Check: resource.ComposeTestCheckFunc( testAccDataSourceGoogleNetworkCheck("data.google_compute_network.my_network", "google_compute_network.foobar"), ), @@ -57,12 +60,14 @@ func testAccDataSourceGoogleNetworkCheck(data_source_name string, resource_name } } -var TestAccDataSourceGoogleNetworkConfig = ` +func testAccDataSourceGoogleNetworkConfig(name string) string { + return fmt.Sprintf(` resource "google_compute_network" "foobar" { - name = "network-test" + name = "%s" description = "my-description" } data "google_compute_network" "my_network" { name = "${google_compute_network.foobar.name}" -}` +}`, name) +} diff --git a/builtin/providers/google/resource_compute_project_metadata_test.go b/builtin/providers/google/resource_compute_project_metadata_test.go index 7be3dfb263..b0bfa0ea18 100644 --- a/builtin/providers/google/resource_compute_project_metadata_test.go +++ b/builtin/providers/google/resource_compute_project_metadata_test.go @@ -2,8 +2,10 @@ package google import ( "fmt" + "os" "testing" + "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" "google.golang.org/api/compute/v1" @@ -11,7 +13,16 @@ import ( // Add two key value pairs func TestAccComputeProjectMetadata_basic(t *testing.T) { + skipIfEnvNotSet(t, + []string{ + "GOOGLE_ORG", + "GOOGLE_BILLING_ACCOUNT", + }..., + ) + + billingId := os.Getenv("GOOGLE_BILLING_ACCOUNT") var project compute.Project + projectID := "terrafom-test-" + acctest.RandString(10) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -19,13 +30,13 @@ func TestAccComputeProjectMetadata_basic(t *testing.T) { CheckDestroy: testAccCheckComputeProjectMetadataDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccComputeProject_basic0_metadata, + Config: testAccComputeProject_basic0_metadata(projectID, pname, org, billingId), Check: resource.ComposeTestCheckFunc( testAccCheckComputeProjectExists( - "google_compute_project_metadata.fizzbuzz", &project), - testAccCheckComputeProjectMetadataContains(&project, "banana", "orange"), - testAccCheckComputeProjectMetadataContains(&project, "sofa", "darwinism"), - testAccCheckComputeProjectMetadataSize(&project, 2), + "google_compute_project_metadata.fizzbuzz", projectID, &project), + testAccCheckComputeProjectMetadataContains(projectID, "banana", "orange"), + testAccCheckComputeProjectMetadataContains(projectID, "sofa", "darwinism"), + testAccCheckComputeProjectMetadataSize(projectID, 2), ), }, }, @@ -34,7 +45,16 @@ func TestAccComputeProjectMetadata_basic(t *testing.T) { // Add three key value pairs, then replace one and modify a second func TestAccComputeProjectMetadata_modify_1(t *testing.T) { + skipIfEnvNotSet(t, + []string{ + "GOOGLE_ORG", + "GOOGLE_BILLING_ACCOUNT", + }..., + ) + + billingId := os.Getenv("GOOGLE_BILLING_ACCOUNT") var project compute.Project + projectID := "terrafom-test-" + acctest.RandString(10) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -42,26 +62,26 @@ func TestAccComputeProjectMetadata_modify_1(t *testing.T) { CheckDestroy: testAccCheckComputeProjectMetadataDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccComputeProject_modify0_metadata, + Config: testAccComputeProject_modify0_metadata(projectID, pname, org, billingId), Check: resource.ComposeTestCheckFunc( testAccCheckComputeProjectExists( - "google_compute_project_metadata.fizzbuzz", &project), - testAccCheckComputeProjectMetadataContains(&project, "paper", "pen"), - testAccCheckComputeProjectMetadataContains(&project, "genghis_khan", "french bread"), - testAccCheckComputeProjectMetadataContains(&project, "happy", "smiling"), - testAccCheckComputeProjectMetadataSize(&project, 3), + "google_compute_project_metadata.fizzbuzz", projectID, &project), + testAccCheckComputeProjectMetadataContains(projectID, "paper", "pen"), + testAccCheckComputeProjectMetadataContains(projectID, "genghis_khan", "french bread"), + testAccCheckComputeProjectMetadataContains(projectID, "happy", "smiling"), + testAccCheckComputeProjectMetadataSize(projectID, 3), ), }, resource.TestStep{ - Config: testAccComputeProject_modify1_metadata, + Config: testAccComputeProject_modify1_metadata(projectID, pname, org, billingId), Check: resource.ComposeTestCheckFunc( testAccCheckComputeProjectExists( - "google_compute_project_metadata.fizzbuzz", &project), - testAccCheckComputeProjectMetadataContains(&project, "paper", "pen"), - testAccCheckComputeProjectMetadataContains(&project, "paris", "french bread"), - testAccCheckComputeProjectMetadataContains(&project, "happy", "laughing"), - testAccCheckComputeProjectMetadataSize(&project, 3), + "google_compute_project_metadata.fizzbuzz", projectID, &project), + testAccCheckComputeProjectMetadataContains(projectID, "paper", "pen"), + testAccCheckComputeProjectMetadataContains(projectID, "paris", "french bread"), + testAccCheckComputeProjectMetadataContains(projectID, "happy", "laughing"), + testAccCheckComputeProjectMetadataSize(projectID, 3), ), }, }, @@ -70,7 +90,16 @@ func TestAccComputeProjectMetadata_modify_1(t *testing.T) { // Add two key value pairs, and replace both func TestAccComputeProjectMetadata_modify_2(t *testing.T) { + skipIfEnvNotSet(t, + []string{ + "GOOGLE_ORG", + "GOOGLE_BILLING_ACCOUNT", + }..., + ) + + billingId := os.Getenv("GOOGLE_BILLING_ACCOUNT") var project compute.Project + projectID := "terraform-test-" + acctest.RandString(10) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -78,24 +107,24 @@ func TestAccComputeProjectMetadata_modify_2(t *testing.T) { CheckDestroy: testAccCheckComputeProjectMetadataDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccComputeProject_basic0_metadata, + Config: testAccComputeProject_basic0_metadata(projectID, pname, org, billingId), Check: resource.ComposeTestCheckFunc( testAccCheckComputeProjectExists( - "google_compute_project_metadata.fizzbuzz", &project), - testAccCheckComputeProjectMetadataContains(&project, "banana", "orange"), - testAccCheckComputeProjectMetadataContains(&project, "sofa", "darwinism"), - testAccCheckComputeProjectMetadataSize(&project, 2), + "google_compute_project_metadata.fizzbuzz", projectID, &project), + testAccCheckComputeProjectMetadataContains(projectID, "banana", "orange"), + testAccCheckComputeProjectMetadataContains(projectID, "sofa", "darwinism"), + testAccCheckComputeProjectMetadataSize(projectID, 2), ), }, resource.TestStep{ - Config: testAccComputeProject_basic1_metadata, + Config: testAccComputeProject_basic1_metadata(projectID, pname, org, billingId), Check: resource.ComposeTestCheckFunc( testAccCheckComputeProjectExists( - "google_compute_project_metadata.fizzbuzz", &project), - testAccCheckComputeProjectMetadataContains(&project, "kiwi", "papaya"), - testAccCheckComputeProjectMetadataContains(&project, "finches", "darwinism"), - testAccCheckComputeProjectMetadataSize(&project, 2), + "google_compute_project_metadata.fizzbuzz", projectID, &project), + testAccCheckComputeProjectMetadataContains(projectID, "kiwi", "papaya"), + testAccCheckComputeProjectMetadataContains(projectID, "finches", "darwinism"), + testAccCheckComputeProjectMetadataSize(projectID, 2), ), }, }, @@ -105,15 +134,21 @@ func TestAccComputeProjectMetadata_modify_2(t *testing.T) { func testAccCheckComputeProjectMetadataDestroy(s *terraform.State) error { config := testAccProvider.Meta().(*Config) - project, err := config.clientCompute.Projects.Get(config.Project).Do() - if err == nil && len(project.CommonInstanceMetadata.Items) > 0 { - return fmt.Errorf("Error, metadata items still exist") + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_compute_project_metadata" { + continue + } + + project, err := config.clientCompute.Projects.Get(rs.Primary.ID).Do() + if err == nil && len(project.CommonInstanceMetadata.Items) > 0 { + return fmt.Errorf("Error, metadata items still exist in %s", rs.Primary.ID) + } } return nil } -func testAccCheckComputeProjectExists(n string, project *compute.Project) resource.TestCheckFunc { +func testAccCheckComputeProjectExists(n, projectID string, project *compute.Project) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -126,8 +161,7 @@ func testAccCheckComputeProjectExists(n string, project *compute.Project) resour config := testAccProvider.Meta().(*Config) - found, err := config.clientCompute.Projects.Get( - config.Project).Do() + found, err := config.clientCompute.Projects.Get(projectID).Do() if err != nil { return err } @@ -142,10 +176,10 @@ func testAccCheckComputeProjectExists(n string, project *compute.Project) resour } } -func testAccCheckComputeProjectMetadataContains(project *compute.Project, key string, value string) resource.TestCheckFunc { +func testAccCheckComputeProjectMetadataContains(projectID, key, value string) resource.TestCheckFunc { return func(s *terraform.State) error { config := testAccProvider.Meta().(*Config) - project, err := config.clientCompute.Projects.Get(config.Project).Do() + project, err := config.clientCompute.Projects.Get(projectID).Do() if err != nil { return fmt.Errorf("Error, failed to load project service for %s: %s", config.Project, err) } @@ -161,14 +195,14 @@ func testAccCheckComputeProjectMetadataContains(project *compute.Project, key st } } - return fmt.Errorf("Error, key %s not present", key) + return fmt.Errorf("Error, key %s not present in %s", key, project.SelfLink) } } -func testAccCheckComputeProjectMetadataSize(project *compute.Project, size int) resource.TestCheckFunc { +func testAccCheckComputeProjectMetadataSize(projectID string, size int) resource.TestCheckFunc { return func(s *terraform.State) error { config := testAccProvider.Meta().(*Config) - project, err := config.clientCompute.Projects.Get(config.Project).Do() + project, err := config.clientCompute.Projects.Get(projectID).Do() if err != nil { return fmt.Errorf("Error, failed to load project service for %s: %s", config.Project, err) } @@ -182,36 +216,100 @@ func testAccCheckComputeProjectMetadataSize(project *compute.Project, size int) } } -const testAccComputeProject_basic0_metadata = ` -resource "google_compute_project_metadata" "fizzbuzz" { - metadata { - banana = "orange" - sofa = "darwinism" - } -}` +func testAccComputeProject_basic0_metadata(projectID, name, org, billing string) string { + return fmt.Sprintf(` +resource "google_project" "project" { + project_id = "%s" + name = "%s" + org_id = "%s" + billing_account = "%s" +} -const testAccComputeProject_basic1_metadata = ` -resource "google_compute_project_metadata" "fizzbuzz" { - metadata { - kiwi = "papaya" - finches = "darwinism" - } -}` +resource "google_project_services" "services" { + project = "${google_project.project.project_id}" + services = ["compute-component.googleapis.com"] +} -const testAccComputeProject_modify0_metadata = ` resource "google_compute_project_metadata" "fizzbuzz" { - metadata { - paper = "pen" - genghis_khan = "french bread" - happy = "smiling" - } -}` + project = "${google_project.project.project_id}" + metadata { + banana = "orange" + sofa = "darwinism" + } + depends_on = ["google_project_services.services"] +}`, projectID, name, org, billing) +} + +func testAccComputeProject_basic1_metadata(projectID, name, org, billing string) string { + return fmt.Sprintf(` +resource "google_project" "project" { + project_id = "%s" + name = "%s" + org_id = "%s" + billing_account = "%s" +} + +resource "google_project_services" "services" { + project = "${google_project.project.project_id}" + services = ["compute-component.googleapis.com"] +} -const testAccComputeProject_modify1_metadata = ` resource "google_compute_project_metadata" "fizzbuzz" { - metadata { - paper = "pen" - paris = "french bread" - happy = "laughing" - } -}` + project = "${google_project.project.project_id}" + metadata { + kiwi = "papaya" + finches = "darwinism" + } + depends_on = ["google_project_services.services"] +}`, projectID, name, org, billing) +} + +func testAccComputeProject_modify0_metadata(projectID, name, org, billing string) string { + return fmt.Sprintf(` +resource "google_project" "project" { + project_id = "%s" + name = "%s" + org_id = "%s" + billing_account = "%s" +} + +resource "google_project_services" "services" { + project = "${google_project.project.project_id}" + services = ["compute-component.googleapis.com"] +} + +resource "google_compute_project_metadata" "fizzbuzz" { + project = "${google_project.project.project_id}" + metadata { + paper = "pen" + genghis_khan = "french bread" + happy = "smiling" + } + depends_on = ["google_project_services.services"] +}`, projectID, name, org, billing) +} + +func testAccComputeProject_modify1_metadata(projectID, name, org, billing string) string { + return fmt.Sprintf(` +resource "google_project" "project" { + project_id = "%s" + name = "%s" + org_id = "%s" + billing_account = "%s" +} + +resource "google_project_services" "services" { + project = "${google_project.project.project_id}" + services = ["compute-component.googleapis.com"] +} + +resource "google_compute_project_metadata" "fizzbuzz" { + project = "${google_project.project.project_id}" + metadata { + paper = "pen" + paris = "french bread" + happy = "laughing" + } + depends_on = ["google_project_services.services"] +}`, projectID, name, org, billing) +} diff --git a/builtin/providers/google/resource_google_project_services.go b/builtin/providers/google/resource_google_project_services.go index 84bcd95ad9..e284565937 100644 --- a/builtin/providers/google/resource_google_project_services.go +++ b/builtin/providers/google/resource_google_project_services.go @@ -31,6 +31,14 @@ func resourceGoogleProjectServices() *schema.Resource { } } +// These services can only be enabled as a side-effect of enabling other services, +// so don't bother storing them in the config or using them for diffing. +var ignore = map[string]struct{}{ + "containeranalysis.googleapis.com": struct{}{}, + "dataproc-control.googleapis.com": struct{}{}, + "source.googleapis.com": struct{}{}, +} + func resourceGoogleProjectServicesCreate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) pid := d.Get("project").(string) @@ -160,7 +168,9 @@ func getApiServices(pid string, config *Config) ([]string, error) { return apiServices, err } for _, v := range svcResp.Services { - apiServices = append(apiServices, v.ServiceName) + if _, ok := ignore[v.ServiceName]; !ok { + apiServices = append(apiServices, v.ServiceName) + } } return apiServices, nil } diff --git a/builtin/providers/google/resource_google_project_services_test.go b/builtin/providers/google/resource_google_project_services_test.go index dff073b285..155a297c73 100644 --- a/builtin/providers/google/resource_google_project_services_test.go +++ b/builtin/providers/google/resource_google_project_services_test.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "log" + "os" "reflect" "sort" "testing" @@ -123,6 +124,46 @@ func TestAccGoogleProjectServices_authoritative2(t *testing.T) { }) } +// Test that services that can't be enabled on their own (such as dataproc-control.googleapis.com) +// don't end up causing diffs when they are enabled as a side-effect of a different service's +// enablement. +func TestAccGoogleProjectServices_ignoreUnenablableServices(t *testing.T) { + skipIfEnvNotSet(t, + []string{ + "GOOGLE_ORG", + "GOOGLE_BILLING_ACCOUNT", + }..., + ) + + billingId := os.Getenv("GOOGLE_BILLING_ACCOUNT") + pid := "terraform-" + acctest.RandString(10) + services := []string{ + "dataproc.googleapis.com", + // The following services are enabled as a side-effect of dataproc's enablement + "storage-component.googleapis.com", + "deploymentmanager.googleapis.com", + "replicapool.googleapis.com", + "replicapoolupdater.googleapis.com", + "resourceviews.googleapis.com", + "compute-component.googleapis.com", + "container.googleapis.com", + "storage-api.googleapis.com", + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccGoogleProjectAssociateServicesBasic_withBilling(services, pid, pname, org, billingId), + Check: resource.ComposeTestCheckFunc( + testProjectServicesMatch(services, pid), + ), + }, + }, + }) +} + func testAccGoogleProjectAssociateServicesBasic(services []string, pid, name, org string) string { return fmt.Sprintf(` resource "google_project" "acceptance" { @@ -137,6 +178,21 @@ resource "google_project_services" "acceptance" { `, pid, name, org, testStringsToString(services)) } +func testAccGoogleProjectAssociateServicesBasic_withBilling(services []string, pid, name, org, billing string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + billing_account = "%s" +} +resource "google_project_services" "acceptance" { + project = "${google_project.acceptance.project_id}" + services = [%s] +} +`, pid, name, org, billing, testStringsToString(services)) +} + func testProjectServicesMatch(services []string, pid string) resource.TestCheckFunc { return func(s *terraform.State) error { config := testAccProvider.Meta().(*Config) diff --git a/builtin/providers/heroku/provider.go b/builtin/providers/heroku/provider.go index 6a8c9b986b..08432ac98b 100644 --- a/builtin/providers/heroku/provider.go +++ b/builtin/providers/heroku/provider.go @@ -1,7 +1,9 @@ package heroku import ( + "fmt" "log" + "strings" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" @@ -25,12 +27,13 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "heroku_addon": resourceHerokuAddon(), - "heroku_app": resourceHerokuApp(), - "heroku_cert": resourceHerokuCert(), - "heroku_domain": resourceHerokuDomain(), - "heroku_drain": resourceHerokuDrain(), - "heroku_space": resourceHerokuSpace(), + "heroku_addon": resourceHerokuAddon(), + "heroku_app": resourceHerokuApp(), + "heroku_app_feature": resourceHerokuAppFeature(), + "heroku_cert": resourceHerokuCert(), + "heroku_domain": resourceHerokuDomain(), + "heroku_drain": resourceHerokuDrain(), + "heroku_space": resourceHerokuSpace(), }, ConfigureFunc: providerConfigure, @@ -46,3 +49,12 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { log.Println("[INFO] Initializing Heroku client") return config.Client() } + +func buildCompositeID(a, b string) string { + return fmt.Sprintf("%s:%s", a, b) +} + +func parseCompositeID(id string) (string, string) { + parts := strings.SplitN(id, ":", 2) + return parts[0], parts[1] +} diff --git a/builtin/providers/heroku/resource_heroku_app_feature.go b/builtin/providers/heroku/resource_heroku_app_feature.go new file mode 100644 index 0000000000..9718fdc672 --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_app_feature.go @@ -0,0 +1,101 @@ +package heroku + +import ( + "context" + "log" + + heroku "github.com/cyberdelia/heroku-go/v3" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceHerokuAppFeature() *schema.Resource { + return &schema.Resource{ + Create: resourceHerokuAppFeatureCreate, + Update: resourceHerokuAppFeatureUpdate, + Read: resourceHerokuAppFeatureRead, + Delete: resourceHerokuAppFeatureDelete, + + Schema: map[string]*schema.Schema{ + "app": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + }, + } +} + +func resourceHerokuAppFeatureRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + app, id := parseCompositeID(d.Id()) + + feature, err := client.AppFeatureInfo(context.TODO(), app, id) + if err != nil { + return err + } + + d.Set("app", app) + d.Set("name", feature.Name) + d.Set("enabled", feature.Enabled) + + return nil +} + +func resourceHerokuAppFeatureCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + app := d.Get("app").(string) + featureName := d.Get("name").(string) + enabled := d.Get("enabled").(bool) + + opts := heroku.AppFeatureUpdateOpts{Enabled: enabled} + + log.Printf("[DEBUG] Feature set configuration: %#v, %#v", featureName, opts) + + feature, err := client.AppFeatureUpdate(context.TODO(), app, featureName, opts) + if err != nil { + return err + } + + d.SetId(buildCompositeID(app, feature.ID)) + + return resourceHerokuAppFeatureRead(d, meta) +} + +func resourceHerokuAppFeatureUpdate(d *schema.ResourceData, meta interface{}) error { + if d.HasChange("enabled") { + return resourceHerokuAppFeatureCreate(d, meta) + } + + return resourceHerokuAppFeatureRead(d, meta) +} + +func resourceHerokuAppFeatureDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + app, id := parseCompositeID(d.Id()) + featureName := d.Get("name").(string) + + log.Printf("[INFO] Deleting app feature %s (%s) for app %s", featureName, id, app) + opts := heroku.AppFeatureUpdateOpts{Enabled: false} + _, err := client.AppFeatureUpdate(context.TODO(), app, id, opts) + if err != nil { + return err + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/heroku/resource_heroku_app_feature_test.go b/builtin/providers/heroku/resource_heroku_app_feature_test.go new file mode 100644 index 0000000000..870216ae94 --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_app_feature_test.go @@ -0,0 +1,135 @@ +package heroku + +import ( + "context" + "fmt" + "testing" + + heroku "github.com/cyberdelia/heroku-go/v3" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccHerokuAppFeature(t *testing.T) { + var feature heroku.AppFeatureInfoResult + appName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuFeatureDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckHerokuFeature_basic(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuFeatureExists("heroku_app_feature.runtime_metrics", &feature), + testAccCheckHerokuFeatureEnabled(&feature, true), + resource.TestCheckResourceAttr( + "heroku_app_feature.runtime_metrics", "enabled", "true", + ), + ), + }, + { + Config: testAccCheckHerokuFeature_disabled(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuFeatureExists("heroku_app_feature.runtime_metrics", &feature), + testAccCheckHerokuFeatureEnabled(&feature, false), + resource.TestCheckResourceAttr( + "heroku_app_feature.runtime_metrics", "enabled", "false", + ), + ), + }, + }, + }) +} + +func testAccCheckHerokuFeatureDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*heroku.Service) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "heroku_app_feature" { + continue + } + + _, err := client.AppFeatureInfo(context.TODO(), rs.Primary.Attributes["app"], rs.Primary.ID) + + if err == nil { + return fmt.Errorf("Feature still exists") + } + } + + return nil +} + +func testAccCheckHerokuFeatureExists(n string, feature *heroku.AppFeatureInfoResult) 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 feature ID is set") + } + + app, id := parseCompositeID(rs.Primary.ID) + if app != rs.Primary.Attributes["app"] { + return fmt.Errorf("Bad app: %s", app) + } + + client := testAccProvider.Meta().(*heroku.Service) + + foundFeature, err := client.AppFeatureInfo(context.TODO(), app, id) + if err != nil { + return err + } + + if foundFeature.ID != id { + return fmt.Errorf("Feature not found") + } + + *feature = *foundFeature + return nil + } +} + +func testAccCheckHerokuFeatureEnabled(feature *heroku.AppFeatureInfoResult, enabled bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + if feature.Enabled != enabled { + return fmt.Errorf("Bad enabled: %v", feature.Enabled) + } + + return nil + } +} + +func testAccCheckHerokuFeature_basic(appName string) string { + return fmt.Sprintf(` +resource "heroku_app" "example" { + name = "%s" + region = "us" +} + +resource "heroku_app_feature" "runtime_metrics" { + app = "${heroku_app.example.name}" + name = "log-runtime-metrics" +} +`, appName) +} + +func testAccCheckHerokuFeature_disabled(appName string) string { + return fmt.Sprintf(` +resource "heroku_app" "example" { + name = "%s" + region = "us" +} + +resource "heroku_app_feature" "runtime_metrics" { + app = "${heroku_app.example.name}" + name = "log-runtime-metrics" + enabled = false +} +`, appName) +} diff --git a/builtin/providers/heroku/resource_heroku_space.go b/builtin/providers/heroku/resource_heroku_space.go index 3e90fffbbb..fcabee4298 100644 --- a/builtin/providers/heroku/resource_heroku_space.go +++ b/builtin/providers/heroku/resource_heroku_space.go @@ -2,9 +2,12 @@ package heroku import ( "context" + "fmt" "log" + "time" heroku "github.com/cyberdelia/heroku-go/v3" + "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" ) @@ -56,23 +59,32 @@ func resourceHerokuSpaceCreate(d *schema.ResourceData, meta interface{}) error { d.SetId(space.ID) log.Printf("[INFO] Space ID: %s", d.Id()) - // The type conversion here can be dropped when the vendored version of - // heroku-go is updated. - setSpaceAttributes(d, (*heroku.Space)(space)) - return nil + // Wait for the Space to be allocated + log.Printf("[DEBUG] Waiting for Space (%s) to be allocated", d.Id()) + stateConf := &resource.StateChangeConf{ + Pending: []string{"allocating"}, + Target: []string{"allocated"}, + Refresh: SpaceStateRefreshFunc(client, d.Id()), + Timeout: 20 * time.Minute, + } + + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for Space (%s) to become available: %s", d.Id(), err) + } + + return resourceHerokuSpaceRead(d, meta) } func resourceHerokuSpaceRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*heroku.Service) - space, err := client.SpaceInfo(context.TODO(), d.Id()) + spaceRaw, _, err := SpaceStateRefreshFunc(client, d.Id())() if err != nil { return err } + space := spaceRaw.(*heroku.Space) - // The type conversion here can be dropped when the vendored version of - // heroku-go is updated. - setSpaceAttributes(d, (*heroku.Space)(space)) + setSpaceAttributes(d, space) return nil } @@ -115,3 +127,18 @@ func resourceHerokuSpaceDelete(d *schema.ResourceData, meta interface{}) error { d.SetId("") return nil } + +// SpaceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch +// a Space. +func SpaceStateRefreshFunc(client *heroku.Service, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + space, err := client.SpaceInfo(context.TODO(), id) + if err != nil { + return nil, "", err + } + + // The type conversion here can be dropped when the vendored version of + // heroku-go is updated. + return (*heroku.Space)(space), space.State, nil + } +} diff --git a/builtin/providers/nomad/provider.go b/builtin/providers/nomad/provider.go index d23c4ad97f..61f8603bcb 100644 --- a/builtin/providers/nomad/provider.go +++ b/builtin/providers/nomad/provider.go @@ -24,6 +24,24 @@ func Provider() terraform.ResourceProvider { DefaultFunc: schema.EnvDefaultFunc("NOMAD_REGION", ""), Description: "Region of the target Nomad agent.", }, + "ca_file": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("NOMAD_CACERT", ""), + Description: "A path to a PEM-encoded certificate authority used to verify the remote agent's certificate.", + }, + "cert_file": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("NOMAD_CLIENT_CERT", ""), + Description: "A path to a PEM-encoded certificate provided to the remote agent; requires use of key_file.", + }, + "key_file": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("NOMAD_CLIENT_KEY", ""), + Description: "A path to a PEM-encoded private key, required if cert_file is specified.", + }, }, ConfigureFunc: providerConfigure, @@ -38,6 +56,9 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { config := api.DefaultConfig() config.Address = d.Get("address").(string) config.Region = d.Get("region").(string) + config.TLSConfig.CACert = d.Get("ca_file").(string) + config.TLSConfig.ClientCert = d.Get("cert_file").(string) + config.TLSConfig.ClientKey = d.Get("key_file").(string) client, err := api.NewClient(config) if err != nil { diff --git a/builtin/providers/test/data_source_test.go b/builtin/providers/test/data_source_test.go index 3f4e5ada61..77e235f3d9 100644 --- a/builtin/providers/test/data_source_test.go +++ b/builtin/providers/test/data_source_test.go @@ -99,3 +99,59 @@ resource "test_resource" "foo" { }, }) } + +// TestDataSource_dataSourceCountGrandChild tests that a grandchild data source +// that is based off of count works, ie: dependency chain foo -> bar -> baz. +// This was failing because CountBoundaryTransformer is being run during apply +// instead of plan, which meant that it wasn't firing after data sources were +// potentially changing state and causing diff/interpolation issues. +// +// This happens after the initial apply, after state is saved. +func TestDataSource_dataSourceCountGrandChild(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + Providers: testAccProviders, + CheckDestroy: func(s *terraform.State) error { + return nil + }, + Steps: []resource.TestStep{ + { + Config: dataSourceCountGrandChildConfig, + }, + { + Config: dataSourceCountGrandChildConfig, + Check: func(s *terraform.State) error { + for _, v := range []string{"foo", "bar", "baz"} { + count := 0 + for k := range s.RootModule().Resources { + if strings.HasPrefix(k, fmt.Sprintf("data.test_data_source.%s.", v)) { + count++ + } + } + + if count != 2 { + return fmt.Errorf("bad count for data.test_data_source.%s: %d", v, count) + } + } + return nil + }, + }, + }, + }) +} + +const dataSourceCountGrandChildConfig = ` +data "test_data_source" "foo" { + count = 2 + input = "one" +} + +data "test_data_source" "bar" { + count = "${length(data.test_data_source.foo.*.id)}" + input = "${data.test_data_source.foo.*.output[count.index]}" +} + +data "test_data_source" "baz" { + count = "${length(data.test_data_source.bar.*.id)}" + input = "${data.test_data_source.bar.*.output[count.index]}" +} +` diff --git a/builtin/providers/triton/provider.go b/builtin/providers/triton/provider.go index 4c21722c27..8a56b5dc46 100644 --- a/builtin/providers/triton/provider.go +++ b/builtin/providers/triton/provider.go @@ -42,6 +42,12 @@ func Provider() terraform.ResourceProvider { Required: true, DefaultFunc: schema.MultiEnvDefaultFunc([]string{"TRITON_KEY_ID", "SDC_KEY_ID"}, ""), }, + + "insecure_skip_tls_verify": { + Type: schema.TypeBool, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("TRITON_SKIP_TLS_VERIFY", ""), + }, }, ResourcesMap: map[string]*schema.Resource{ @@ -56,10 +62,11 @@ func Provider() terraform.ResourceProvider { } type Config struct { - Account string - KeyMaterial string - KeyID string - URL string + Account string + KeyMaterial string + KeyID string + URL string + InsecureSkipTLSVerify bool } func (c Config) validate() error { @@ -98,6 +105,10 @@ func (c Config) getTritonClient() (*triton.Client, error) { return nil, errwrap.Wrapf("Error Creating Triton Client: {{err}}", err) } + if c.InsecureSkipTLSVerify { + client.InsecureSkipTLSVerify() + } + return client, nil } @@ -106,6 +117,8 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { Account: d.Get("account").(string), URL: d.Get("url").(string), KeyID: d.Get("key_id").(string), + + InsecureSkipTLSVerify: d.Get("insecure_skip_tls_verify").(bool), } if keyMaterial, ok := d.GetOk("key_material"); ok { diff --git a/builtin/providers/triton/resource_machine.go b/builtin/providers/triton/resource_machine.go index 7eb66591c5..85b26a8a53 100644 --- a/builtin/providers/triton/resource_machine.go +++ b/builtin/providers/triton/resource_machine.go @@ -23,6 +23,7 @@ var ( "user_script": "user-script", "user_data": "user-data", "administrator_pw": "administrator-pw", + "cloud_config": "cloud-init:user-data", } ) @@ -182,6 +183,12 @@ func resourceMachine() *schema.Resource { Optional: true, Computed: true, }, + "cloud_config": { + Description: "copied to machine on boot", + Type: schema.TypeString, + Optional: true, + Computed: true, + }, "user_data": { Description: "Data copied to machine on boot", Type: schema.TypeString, diff --git a/command/env_command.go b/command/env_command.go index 9548122a10..f29c9456f6 100644 --- a/command/env_command.go +++ b/command/env_command.go @@ -57,7 +57,7 @@ const ( envDoesNotExist = ` Environment %q doesn't exist! -You can create this environment with the "-new" option.` +You can create this environment with the "new" option.` envChanged = `[reset][green]Switched to environment %q!` diff --git a/terraform/context_plan_test.go b/terraform/context_plan_test.go index 3e8c541900..3851e406bd 100644 --- a/terraform/context_plan_test.go +++ b/terraform/context_plan_test.go @@ -3154,3 +3154,146 @@ func TestContext2Plan_ignoreChangesWithFlatmaps(t *testing.T) { t.Fatalf("bad:\n%s\n\nexpected\n\n%s", actual, expected) } } + +// TestContext2Plan_resourceNestedCount ensures resource sets that depend on +// the count of another resource set (ie: count of a data source that depends +// on another data source's instance count - data.x.foo.*.id) get properly +// normalized to the indexes they should be. This case comes up when there is +// an existing state (after an initial apply). +func TestContext2Plan_resourceNestedCount(t *testing.T) { + m := testModule(t, "nested-resource-count-plan") + p := testProvider("aws") + p.DiffFn = testDiffFn + p.RefreshFn = func(i *InstanceInfo, is *InstanceState) (*InstanceState, error) { + return is, nil + } + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo.0": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo0", + Attributes: map[string]string{ + "id": "foo0", + }, + }, + }, + "aws_instance.foo.1": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo1", + Attributes: map[string]string{ + "id": "foo1", + }, + }, + }, + "aws_instance.bar.0": &ResourceState{ + Type: "aws_instance", + Dependencies: []string{"aws_instance.foo.*"}, + Primary: &InstanceState{ + ID: "bar0", + Attributes: map[string]string{ + "id": "bar0", + }, + }, + }, + "aws_instance.bar.1": &ResourceState{ + Type: "aws_instance", + Dependencies: []string{"aws_instance.foo.*"}, + Primary: &InstanceState{ + ID: "bar1", + Attributes: map[string]string{ + "id": "bar1", + }, + }, + }, + "aws_instance.baz.0": &ResourceState{ + Type: "aws_instance", + Dependencies: []string{"aws_instance.bar.*"}, + Primary: &InstanceState{ + ID: "baz0", + Attributes: map[string]string{ + "id": "baz0", + }, + }, + }, + "aws_instance.baz.1": &ResourceState{ + Type: "aws_instance", + Dependencies: []string{"aws_instance.bar.*"}, + Primary: &InstanceState{ + ID: "baz1", + Attributes: map[string]string{ + "id": "baz1", + }, + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + w, e := ctx.Validate() + if len(w) > 0 { + t.Fatalf("warnings generated on validate: %#v", w) + } + if len(e) > 0 { + t.Fatalf("errors generated on validate: %#v", e) + } + + _, err := ctx.Refresh() + if err != nil { + t.Fatalf("refresh err: %s", err) + } + + plan, err := ctx.Plan() + if err != nil { + t.Fatalf("plan err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(` +DIFF: + + + +STATE: + +aws_instance.bar.0: + ID = bar0 + + Dependencies: + aws_instance.foo.* +aws_instance.bar.1: + ID = bar1 + + Dependencies: + aws_instance.foo.* +aws_instance.baz.0: + ID = baz0 + + Dependencies: + aws_instance.bar.* +aws_instance.baz.1: + ID = baz1 + + Dependencies: + aws_instance.bar.* +aws_instance.foo.0: + ID = foo0 +aws_instance.foo.1: + ID = foo1 +`) + if actual != expected { + t.Fatalf("bad:\n%s\n\nexpected\n\n%s", actual, expected) + } +} diff --git a/terraform/graph_builder_plan.go b/terraform/graph_builder_plan.go index 02d869700e..a6a3a90d48 100644 --- a/terraform/graph_builder_plan.go +++ b/terraform/graph_builder_plan.go @@ -113,6 +113,9 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { // have to connect again later for providers and so on. &ReferenceTransformer{}, + // Add the node to fix the state count boundaries + &CountBoundaryTransformer{}, + // Target &TargetsTransformer{Targets: b.Targets}, diff --git a/terraform/graph_builder_plan_test.go b/terraform/graph_builder_plan_test.go index 23526a9ac3..25578ebaf5 100644 --- a/terraform/graph_builder_plan_test.go +++ b/terraform/graph_builder_plan_test.go @@ -29,7 +29,7 @@ func TestPlanGraphBuilder(t *testing.T) { actual := strings.TrimSpace(g.String()) expected := strings.TrimSpace(testPlanGraphBuilderStr) if actual != expected { - t.Fatalf("bad: %s", actual) + t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual) } } @@ -61,6 +61,14 @@ aws_load_balancer.weblb provider.aws aws_security_group.firewall provider.aws +meta.count-boundary (count boundary fixup) + aws_instance.web + aws_load_balancer.weblb + aws_security_group.firewall + openstack_floating_ip.random + provider.aws + provider.openstack + var.foo openstack_floating_ip.random provider.openstack provider.aws @@ -75,6 +83,7 @@ provider.openstack (close) openstack_floating_ip.random provider.openstack root + meta.count-boundary (count boundary fixup) provider.aws (close) provider.openstack (close) var.foo diff --git a/terraform/test-fixtures/nested-resource-count-plan/main.tf b/terraform/test-fixtures/nested-resource-count-plan/main.tf new file mode 100644 index 0000000000..f803fd1f65 --- /dev/null +++ b/terraform/test-fixtures/nested-resource-count-plan/main.tf @@ -0,0 +1,11 @@ +resource "aws_instance" "foo" { + count = 2 +} + +resource "aws_instance" "bar" { + count = "${length(aws_instance.foo.*.id)}" +} + +resource "aws_instance" "baz" { + count = "${length(aws_instance.bar.*.id)}" +} diff --git a/vendor/github.com/joyent/triton-go/accounts.go b/vendor/github.com/joyent/triton-go/accounts.go index 8049d4e7e4..474cce000c 100644 --- a/vendor/github.com/joyent/triton-go/accounts.go +++ b/vendor/github.com/joyent/triton-go/accounts.go @@ -5,8 +5,9 @@ import ( "net/http" "time" - "github.com/hashicorp/errwrap" "fmt" + + "github.com/hashicorp/errwrap" ) type AccountsClient struct { @@ -40,7 +41,8 @@ type Account struct { type GetAccountInput struct{} func (client *AccountsClient) GetAccount(input *GetAccountInput) (*Account, error) { - respReader, err := client.executeRequest(http.MethodGet, "/my", nil) + path := fmt.Sprintf("/%s", client.accountName) + respReader, err := client.executeRequest(http.MethodGet, path, nil) if respReader != nil { defer respReader.Close() } @@ -58,17 +60,17 @@ func (client *AccountsClient) GetAccount(input *GetAccountInput) (*Account, erro } type UpdateAccountInput struct { - Email string `json:"email,omitempty"` - CompanyName string `json:"companyName,omitempty"` - FirstName string `json:"firstName,omitempty"` - LastName string `json:"lastName,omitempty"` - Address string `json:"address,omitempty"` - PostalCode string `json:"postalCode,omitempty"` - City string `json:"city,omitempty"` - State string `json:"state,omitempty"` - Country string `json:"country,omitempty"` - Phone string `json:"phone,omitempty"` - TritonCNSEnabled bool `json:"triton_cns_enabled,omitempty"` + Email string `json:"email,omitempty"` + CompanyName string `json:"companyName,omitempty"` + FirstName string `json:"firstName,omitempty"` + LastName string `json:"lastName,omitempty"` + Address string `json:"address,omitempty"` + PostalCode string `json:"postalCode,omitempty"` + City string `json:"city,omitempty"` + State string `json:"state,omitempty"` + Country string `json:"country,omitempty"` + Phone string `json:"phone,omitempty"` + TritonCNSEnabled bool `json:"triton_cns_enabled,omitempty"` } // UpdateAccount updates your account details with the given parameters. diff --git a/vendor/github.com/joyent/triton-go/client.go b/vendor/github.com/joyent/triton-go/client.go index e34c6b1fe6..5ee1f0be73 100644 --- a/vendor/github.com/joyent/triton-go/client.go +++ b/vendor/github.com/joyent/triton-go/client.go @@ -2,6 +2,7 @@ package triton import ( "bytes" + "crypto/tls" "encoding/json" "fmt" "io" @@ -45,16 +46,7 @@ func NewClient(endpoint string, accountName string, signers ...authentication.Si } httpClient := &http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).Dial, - TLSHandshakeTimeout: 10 * time.Second, - DisableKeepAlives: true, - MaxIdleConnsPerHost: -1, - }, + Transport: httpTransport(false), CheckRedirect: doNotFollowRedirects, } @@ -75,6 +67,34 @@ func NewClient(endpoint string, accountName string, signers ...authentication.Si }, nil } +// InsecureSkipTLSVerify turns off TLS verification for the client connection. This +// allows connection to an endpoint with a certificate which was signed by a non- +// trusted CA, such as self-signed certificates. This can be useful when connecting +// to temporary Triton installations such as Triton Cloud-On-A-Laptop. +func (c *Client) InsecureSkipTLSVerify() { + if c.client == nil { + return + } + + c.client.HTTPClient.Transport = httpTransport(true) +} + +func httpTransport(insecureSkipTLSVerify bool) *http.Transport { + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + DisableKeepAlives: true, + MaxIdleConnsPerHost: -1, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: insecureSkipTLSVerify, + }, + } +} + func doNotFollowRedirects(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } diff --git a/vendor/github.com/joyent/triton-go/datacenters.go b/vendor/github.com/joyent/triton-go/datacenters.go index c90cc954ab..72ccbd6d91 100644 --- a/vendor/github.com/joyent/triton-go/datacenters.go +++ b/vendor/github.com/joyent/triton-go/datacenters.go @@ -28,7 +28,8 @@ type DataCenter struct { type ListDataCentersInput struct{} func (client *DataCentersClient) ListDataCenters(*ListDataCentersInput) ([]*DataCenter, error) { - respReader, err := client.executeRequest(http.MethodGet, "/my/datacenters", nil) + path := fmt.Sprintf("/%s/datacenters", client.accountName) + respReader, err := client.executeRequest(http.MethodGet, path, nil) if respReader != nil { defer respReader.Close() } @@ -68,7 +69,8 @@ type GetDataCenterInput struct { } func (client *DataCentersClient) GetDataCenter(input *GetDataCenterInput) (*DataCenter, error) { - resp, err := client.executeRequestRaw(http.MethodGet, fmt.Sprintf("/my/datacenters/%s", input.Name), nil) + path := fmt.Sprintf("/%s/datacenters/%s", client.accountName, input.Name) + resp, err := client.executeRequestRaw(http.MethodGet, path, nil) if err != nil { return nil, errwrap.Wrapf("Error executing GetDatacenter request: {{err}}", err) } diff --git a/vendor/github.com/joyent/triton-go/fabrics.go b/vendor/github.com/joyent/triton-go/fabrics.go index 5404d10bba..5b1caa5211 100644 --- a/vendor/github.com/joyent/triton-go/fabrics.go +++ b/vendor/github.com/joyent/triton-go/fabrics.go @@ -27,7 +27,8 @@ type FabricVLAN struct { type ListFabricVLANsInput struct{} func (client *FabricsClient) ListFabricVLANs(*ListFabricVLANsInput) ([]*FabricVLAN, error) { - respReader, err := client.executeRequest(http.MethodGet, "/my/fabrics/default/vlans", nil) + path := fmt.Sprintf("/%s/fabrics/default/vlans", client.accountName) + respReader, err := client.executeRequest(http.MethodGet, path, nil) if respReader != nil { defer respReader.Close() } diff --git a/vendor/github.com/joyent/triton-go/firewall.go b/vendor/github.com/joyent/triton-go/firewall.go index c91d012d9a..e9d57a80eb 100644 --- a/vendor/github.com/joyent/triton-go/firewall.go +++ b/vendor/github.com/joyent/triton-go/firewall.go @@ -39,7 +39,8 @@ type FirewallRule struct { type ListFirewallRulesInput struct{} func (client *FirewallClient) ListFirewallRules(*ListFirewallRulesInput) ([]*FirewallRule, error) { - respReader, err := client.executeRequest(http.MethodGet, "/my/fwrules", nil) + path := fmt.Sprintf("/%s/fwrules", client.accountName) + respReader, err := client.executeRequest(http.MethodGet, path, nil) if respReader != nil { defer respReader.Close() } @@ -194,7 +195,8 @@ type ListMachineFirewallRulesInput struct { } func (client *FirewallClient) ListMachineFirewallRules(input *ListMachineFirewallRulesInput) ([]*FirewallRule, error) { - respReader, err := client.executeRequest(http.MethodGet, fmt.Sprintf("/my/machines/%s/firewallrules", input.MachineID), nil) + path := fmt.Sprintf("/%s/machines/%s/firewallrules", client.accountName, input.MachineID) + respReader, err := client.executeRequest(http.MethodGet, path, nil) if respReader != nil { defer respReader.Close() } diff --git a/vendor/github.com/joyent/triton-go/images.go b/vendor/github.com/joyent/triton-go/images.go index 3e0aa8a75d..01d6ea96d4 100644 --- a/vendor/github.com/joyent/triton-go/images.go +++ b/vendor/github.com/joyent/triton-go/images.go @@ -49,7 +49,8 @@ type Image struct { type ListImagesInput struct{} func (client *ImagesClient) ListImages(*ListImagesInput) ([]*Image, error) { - respReader, err := client.executeRequest(http.MethodGet, "/my/images", nil) + path := fmt.Sprintf("/%s/images", client.accountName) + respReader, err := client.executeRequest(http.MethodGet, path, nil) if respReader != nil { defer respReader.Close() } @@ -148,7 +149,7 @@ type CreateImageFromMachineInput struct { HomePage string `json:"homepage,omitempty"` EULA string `json:"eula,omitempty"` ACL []string `json:"acl,omitempty"` - tags map[string]string `json:"tags,omitempty"` + Tags map[string]string `json:"tags,omitempty"` } func (client *ImagesClient) CreateImageFromMachine(input *CreateImageFromMachineInput) (*Image, error) { @@ -178,7 +179,7 @@ type UpdateImageInput struct { HomePage string `json:"homepage,omitempty"` EULA string `json:"eula,omitempty"` ACL []string `json:"acl,omitempty"` - tags map[string]string `json:"tags,omitempty"` + Tags map[string]string `json:"tags,omitempty"` } func (client *ImagesClient) UpdateImage(input *UpdateImageInput) (*Image, error) { diff --git a/vendor/github.com/joyent/triton-go/keys.go b/vendor/github.com/joyent/triton-go/keys.go index a4f394a21b..e633023492 100644 --- a/vendor/github.com/joyent/triton-go/keys.go +++ b/vendor/github.com/joyent/triton-go/keys.go @@ -35,7 +35,8 @@ type ListKeysInput struct{} // ListKeys lists all public keys we have on record for the specified // account. func (client *KeysClient) ListKeys(*ListKeysInput) ([]*Key, error) { - respReader, err := client.executeRequest(http.MethodGet, "/my/keys", nil) + path := fmt.Sprintf("/%s/keys") + respReader, err := client.executeRequest(http.MethodGet, path, nil) if respReader != nil { defer respReader.Close() } diff --git a/vendor/github.com/joyent/triton-go/machines.go b/vendor/github.com/joyent/triton-go/machines.go index 0fae69b4a8..287c706c7a 100644 --- a/vendor/github.com/joyent/triton-go/machines.go +++ b/vendor/github.com/joyent/triton-go/machines.go @@ -100,7 +100,7 @@ func (client *MachinesClient) GetMachine(input *GetMachineInput) (*Machine, erro if response != nil { defer response.Body.Close() } - if response.StatusCode == http.StatusNotFound { + if response.StatusCode == http.StatusNotFound || response.StatusCode == http.StatusGone { return nil, &TritonError{ Code: "ResourceNotFound", } @@ -219,7 +219,8 @@ func (input *CreateMachineInput) toAPI() map[string]interface{} { } func (client *MachinesClient) CreateMachine(input *CreateMachineInput) (*Machine, error) { - respReader, err := client.executeRequest(http.MethodPost, "/my/machines", input.toAPI()) + path := fmt.Sprintf("/%s/machines", client.accountName) + respReader, err := client.executeRequest(http.MethodPost, path, input.toAPI()) if respReader != nil { defer respReader.Close() } @@ -501,7 +502,8 @@ type ListNICsInput struct { } func (client *MachinesClient) ListNICs(input *ListNICsInput) ([]*NIC, error) { - respReader, err := client.executeRequest(http.MethodGet, fmt.Sprintf("/my/machines/%s/nics", input.MachineID), nil) + path := fmt.Sprintf("/%s/machines/%s/nics", client.accountName, input.MachineID) + respReader, err := client.executeRequest(http.MethodGet, path, nil) if respReader != nil { defer respReader.Close() } @@ -560,6 +562,48 @@ func (client *MachinesClient) RemoveNIC(input *RemoveNICInput) error { return nil } +type StopMachineInput struct { + MachineID string +} + +func (client *MachinesClient) StopMachine(input *StopMachineInput) error { + path := fmt.Sprintf("/%s/machines/%s", client.accountName, input.MachineID) + + params := &url.Values{} + params.Set("action", "stop") + + respReader, err := client.executeRequestURIParams(http.MethodPost, path, nil, params) + if respReader != nil { + defer respReader.Close() + } + if err != nil { + return errwrap.Wrapf("Error executing StopMachine request: {{err}}", err) + } + + return nil +} + +type StartMachineInput struct { + MachineID string +} + +func (client *MachinesClient) StartMachine(input *StartMachineInput) error { + path := fmt.Sprintf("/%s/machines/%s", client.accountName, input.MachineID) + + params := &url.Values{} + params.Set("action", "start") + + respReader, err := client.executeRequestURIParams(http.MethodPost, path, nil, params) + if respReader != nil { + defer respReader.Close() + } + if err != nil { + return errwrap.Wrapf("Error executing StartMachine request: {{err}}", err) + } + + return nil +} + var reservedMachineCNSTags = map[string]struct{}{ machineCNSTagDisable: {}, machineCNSTagReversePTR: {}, diff --git a/vendor/github.com/joyent/triton-go/networks.go b/vendor/github.com/joyent/triton-go/networks.go index cb1ec17005..440e3bc87a 100644 --- a/vendor/github.com/joyent/triton-go/networks.go +++ b/vendor/github.com/joyent/triton-go/networks.go @@ -36,7 +36,8 @@ type Network struct { type ListNetworksInput struct{} func (client *NetworksClient) ListNetworks(*ListNetworksInput) ([]*Network, error) { - respReader, err := client.executeRequest(http.MethodGet, "/my/networks", nil) + path := fmt.Sprintf("/%s/networks", client.accountName) + respReader, err := client.executeRequest(http.MethodGet, path, nil) if respReader != nil { defer respReader.Close() } diff --git a/vendor/vendor.json b/vendor/vendor.json index 1c0c7ca53e..9e78284fc2 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -2341,16 +2341,16 @@ "revisionTime": "2016-06-16T18:50:15Z" }, { - "checksumSHA1": "2HimxaJVVp2QDVQ0570L71Zd5s4=", + "checksumSHA1": "XsjyaC6eTHUy/n0iuR46TZcgAK8=", "path": "github.com/joyent/triton-go", - "revision": "5db9e2b6a4c1f7ffd2a7e7aa625f42dba956608c", - "revisionTime": "2017-04-12T23:23:58Z" + "revision": "c73729fd38522591909a371c8180ca7090a59ab9", + "revisionTime": "2017-04-28T18:47:44Z" }, { "checksumSHA1": "QzUqkCSn/ZHyIK346xb9V6EBw9U=", "path": "github.com/joyent/triton-go/authentication", - "revision": "66b31a94af28a65e902423879a2820ea34b773fb", - "revisionTime": "2017-03-31T18:12:29Z" + "revision": "c73729fd38522591909a371c8180ca7090a59ab9", + "revisionTime": "2017-04-28T18:47:44Z" }, { "checksumSHA1": "YhQcOsGx8r2S/jkJ0Qt4cZ5BLCU=", diff --git a/website/source/docs/commands/get.html.markdown b/website/source/docs/commands/get.html.markdown index 4666626cb8..a7eb32ad66 100644 --- a/website/source/docs/commands/get.html.markdown +++ b/website/source/docs/commands/get.html.markdown @@ -9,7 +9,7 @@ description: |- # Command: get The `terraform get` command is used to download and update -[modules](/docs/modules/index.html). +[modules](/docs/modules/index.html) mentioned in the root module. ## Usage @@ -28,3 +28,4 @@ The command-line flags are all optional. The list of available flags are: * `-update` - If specified, modules that are already downloaded will be checked for updates and the updates will be downloaded if present. +* `dir` - Sets the path of the [root module](/docs/modules/index.html#definitions). diff --git a/website/source/docs/enterprise/state/collaborating.html.md b/website/source/docs/enterprise/state/collaborating.html.md index a5e799f477..417887e5a4 100755 --- a/website/source/docs/enterprise/state/collaborating.html.md +++ b/website/source/docs/enterprise/state/collaborating.html.md @@ -8,7 +8,7 @@ description: |- # Collaborating on Terraform Remote State -Terraform Enterprise is one of a few options to store [remote state](/docs/enterprise/state). +Terraform Enterprise is one of a few options to store [remote state](/docs/state/remote.html). Remote state gives you the ability to version and collaborate on Terraform changes. It stores information about the changes Terraform makes based on @@ -18,6 +18,5 @@ In order to collaborate safely on remote state, we recommend [creating an organization](/docs/enterprise/organizations/create.html) to manage teams of users. -Then, following a [remote state push](/docs/enterprise/state) you can view state -versions in the changes tab of the environment created under the same name as -the remote state. +Then, following a [Terraform Enterprise Run](/docs/enterprise/runs) or [`apply`](/docs/commands/apply.html) +you can view state versions in the `States` list of the environment. diff --git a/website/source/docs/enterprise/state/index.html.md b/website/source/docs/enterprise/state/index.html.md index 047cfee280..3b8a0589b0 100755 --- a/website/source/docs/enterprise/state/index.html.md +++ b/website/source/docs/enterprise/state/index.html.md @@ -8,17 +8,17 @@ description: |- # State -Terraform stores the state of your managed infrastructure from the last time -Terraform was run. By default this state is stored in a local file named -`terraform.tfstate`, but it can also be stored remotely, which works better in a -team environment. - -Terraform Enterprise is a remote state provider, allowing you to store, version -and collaborate on states. +Terraform Enterprise stores the state of your managed infrastructure from the +last time Terraform was run. The state is stored remotely, which works better in a +team environment, allowing you to store, version and collaborate on state. Remote state gives you more than just easier version control and safer storage. It also allows you to delegate the outputs to other teams. This allows your infrastructure to be more easily broken down into components that multiple teams can access. -Read [more about remote state](https://www.terraform.io/docs/state/remote.html). +Remote state is automatically updated when you run [`apply`](/docs/commands/apply.html) +locally. It is also updated when an `apply` is executed in a [Terraform Enterprise +Run](/docs/enterprise/runs/index.html). + +Read [more about remote state](/docs/state/remote.html). diff --git a/website/source/docs/enterprise/state/pushing.html.md b/website/source/docs/enterprise/state/pushing.html.md old mode 100755 new mode 100644 index 4e9545fda3..ad058d1442 --- a/website/source/docs/enterprise/state/pushing.html.md +++ b/website/source/docs/enterprise/state/pushing.html.md @@ -17,7 +17,9 @@ configuration. To use Terraform Enterprise to store remote state, you'll first need to have the `ATLAS_TOKEN` environment variable set and run the following command. +**NOTE:** `terraform remote config` command has been deprecated in 0.9.X. Remote configuration is now managed as a [backend configuration](/docs/backends/config.html). + ```shell $ terraform remote config \ -backend-config="name=$USERNAME/product" -``` +``` \ No newline at end of file diff --git a/website/source/docs/enterprise/state/resolving-conflicts.html.md b/website/source/docs/enterprise/state/resolving-conflicts.html.md index 0de4069d69..e31a7fafa0 100755 --- a/website/source/docs/enterprise/state/resolving-conflicts.html.md +++ b/website/source/docs/enterprise/state/resolving-conflicts.html.md @@ -35,36 +35,22 @@ operation. ### Using Terraform Locally -Another way to resolve remote state conflicts is to merge and conflicted copies -locally by inspecting the raw state available in the path -`.terraform/terraform.tfstate`. +Another way to resolve remote state conflicts is by manual intervention of the +state file. -When making state changes, it's important to make backup copies in order to -avoid losing any data. +Use the [`state pull`](/docs/commands/state/pull.html) subcommand to pull the +remote state into a local state file. -Any state that is pushed with a serial that is lower than the known serial when -the MD5 of the state does not match will be rejected. - -The serial is embedded in the state file: - -```json -{ - "version": 1, - "serial": 555, - "remote": { - "type": "atlas", - "config": { - "name": "my-username/production" - } - } -} +```shell +$ terraform state pull > example.tfstate ``` Once a conflict has been resolved locally by editing the state file, the serial -can be incremented past the current version and pushed: +can be incremented past the current version and pushed with the +[`state push`](/docs/commands/state/push.html) subcommand: ```shell -$ terraform remote push +$ terraform state push example.tfstate ``` This will upload the manually resolved state and set it as the head version. diff --git a/website/source/docs/modules/index.html.markdown b/website/source/docs/modules/index.html.markdown index cacccf8471..96db650435 100644 --- a/website/source/docs/modules/index.html.markdown +++ b/website/source/docs/modules/index.html.markdown @@ -15,3 +15,8 @@ in Terraform as well as for basic code organization. Modules are very easy to both use and create. Depending on what you're looking to do first, use the navigation on the left to dive into how modules work. + +## Definitions +**Root module** +That is the current working directory when you run [`terraform apply`](/docs/commands/apply.html) or [`get`](/docs/commands/get.html), holding the Terraform [configuration files](/docs/configuration/index.html). +It is itself a valid module. diff --git a/website/source/docs/providers/azurerm/r/container_service.html.markdown b/website/source/docs/providers/azurerm/r/container_service.html.markdown index b06052f7d0..a83121a091 100644 --- a/website/source/docs/providers/azurerm/r/container_service.html.markdown +++ b/website/source/docs/providers/azurerm/r/container_service.html.markdown @@ -44,7 +44,6 @@ resource "azurerm_container_service" "test" { name = "default" count = 1 dns_prefix = "acctestagent1" - fqdn = "you.demo.com" vm_size = "Standard_A0" } @@ -89,7 +88,6 @@ resource "azurerm_container_service" "test" { name = "default" count = 1 dns_prefix = "acctestagent1" - fqdn = "you.demo.com" vm_size = "Standard_A0" } @@ -139,7 +137,6 @@ resource "azurerm_container_service" "test" { name = "default" count = 1 dns_prefix = "acctestagent1" - fqdn = "you.demo.com" vm_size = "Standard_A0" } diff --git a/website/source/docs/providers/azurerm/r/loadbalancer.html.markdown b/website/source/docs/providers/azurerm/r/loadbalancer.html.markdown index e659bf2ec0..7753a5ca52 100644 --- a/website/source/docs/providers/azurerm/r/loadbalancer.html.markdown +++ b/website/source/docs/providers/azurerm/r/loadbalancer.html.markdown @@ -60,6 +60,7 @@ The following arguments are supported: The following attributes are exported: * `id` - The LoadBalancer ID. +* `private_ip_address` - The private IP address assigned to the load balancer, if any. ## Import diff --git a/website/source/docs/providers/google/r/compute_global_forwarding_rule.html.markdown b/website/source/docs/providers/google/r/compute_global_forwarding_rule.html.markdown index df04a69350..bc5607da05 100644 --- a/website/source/docs/providers/google/r/compute_global_forwarding_rule.html.markdown +++ b/website/source/docs/providers/google/r/compute_global_forwarding_rule.html.markdown @@ -80,7 +80,9 @@ The following arguments are supported: * `description` - (Optional) Textual description field. * `ip_address` - (Optional) The static IP. (if not set, an ephemeral IP is - used). + used). This should be the literal IP address to be used, not the `self_link` + to a `google_compute_address` resource. (If using a `google_compute_address` + resource, use the `address` property instead of the `self_link` property.) * `ip_protocol` - (Optional) The IP protocol to route, one of "TCP" "UDP" "AH" "ESP" or "SCTP". (default "TCP"). diff --git a/website/source/docs/providers/heroku/r/app.html.markdown b/website/source/docs/providers/heroku/r/app.html.markdown index 0cf064e4f4..9407eb7799 100644 --- a/website/source/docs/providers/heroku/r/app.html.markdown +++ b/website/source/docs/providers/heroku/r/app.html.markdown @@ -1,7 +1,7 @@ --- layout: "heroku" page_title: "Heroku: heroku_app" -sidebar_current: "docs-heroku-resource-app" +sidebar_current: "docs-heroku-resource-app-x" description: |- Provides a Heroku App resource. This can be used to create and manage applications on Heroku. --- diff --git a/website/source/docs/providers/heroku/r/app_feature.html.markdown b/website/source/docs/providers/heroku/r/app_feature.html.markdown new file mode 100644 index 0000000000..c532b47d7f --- /dev/null +++ b/website/source/docs/providers/heroku/r/app_feature.html.markdown @@ -0,0 +1,28 @@ +--- +layout: "heroku" +page_title: "Heroku: heroku_app_feature" +sidebar_current: "docs-heroku-resource-app-feature" +description: |- + Provides a Heroku App Feature resource. This can be used to create and manage App Features on Heroku. +--- + +# heroku\_app\_feature + +Provides a Heroku App Feature resource. This can be used to create and manage App Features on Heroku. + +## Example Usage + +```hcl +resource "heroku_app_feature" "log_runtime_metrics" { + app = "test-app" + name = "log-runtime-metrics" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `app` - (Required) The Heroku app to link to. +* `name` - (Required) The name of the App Feature to manage. +* `enabled` - (Optional) Whether to enable or disable the App Feature. The default value is true. diff --git a/website/source/docs/providers/nomad/index.html.markdown b/website/source/docs/providers/nomad/index.html.markdown index acf696d2e4..129a9540b4 100644 --- a/website/source/docs/providers/nomad/index.html.markdown +++ b/website/source/docs/providers/nomad/index.html.markdown @@ -34,3 +34,6 @@ The following arguments are supported: * `address` - (Optional) The HTTP(S) API address of the Nomad agent to use. Defaults to `http://127.0.0.1:4646`. The `NOMAD_ADDR` environment variable can also be used. * `region` - (Optional) The Nomad region to target. The `NOMAD_REGION` environment variable can also be used. +* `ca_file` - (Optional) A path to a PEM-encoded certificate authority used to verify the remote agent's certificate. The `NOMAD_CACERT` environment variable can also be used. +* `cert_file` - (Optional) A path to a PEM-encoded certificate provided to the remote agent; requires use of `key_file`. The `NOMAD_CLIENT_CERT` environment variable can also be used. +* `key_file`- (Optional) A path to a PEM-encoded private key, required if `cert_file` is specified. The `NOMAD_CLIENT_KEY` environment variable can also be used. diff --git a/website/source/docs/providers/triton/index.html.markdown b/website/source/docs/providers/triton/index.html.markdown index 7dc866d271..22b9a40e6b 100644 --- a/website/source/docs/providers/triton/index.html.markdown +++ b/website/source/docs/providers/triton/index.html.markdown @@ -33,3 +33,4 @@ The following arguments are supported in the `provider` block: * `key_material` - (Optional) This is the private key of an SSH key associated with the Triton account to be used. If this is not set, the private key corresponding to the fingerprint in `key_id` must be available via an SSH Agent. * `key_id` - (Required) This is the fingerprint of the public key matching the key specified in `key_path`. It can be obtained via the command `ssh-keygen -l -E md5 -f /path/to/key` * `url` - (Optional) This is the URL to the Triton API endpoint. It is required if using a private installation of Triton. The default is to use the Joyent public cloud us-west-1 endpoint. Valid public cloud endpoints include: `us-east-1`, `us-east-2`, `us-east-3`, `us-sw-1`, `us-west-1`, `eu-ams-1` +* `insecure_skip_tls_verify` (Optional - defaults to false) This allows skipping TLS verification of the Triton endpoint. It is useful when connecting to a temporary Triton installation such as Cloud-On-A-Laptop which does not generally use a certificate signed by a trusted root CA. diff --git a/website/source/docs/providers/triton/r/triton_machine.html.markdown b/website/source/docs/providers/triton/r/triton_machine.html.markdown index f01fd8005a..af34fade21 100644 --- a/website/source/docs/providers/triton/r/triton_machine.html.markdown +++ b/website/source/docs/providers/triton/r/triton_machine.html.markdown @@ -77,6 +77,9 @@ The following arguments are supported: * `administrator_pw` - (string) The initial password for the Administrator user. Only used for Windows virtual machines. +* `cloud_config` - (string) + Cloud-init configuration for Linux brand machines, used instead of `user_data`. + The nested `nic` block supports the following: * `network` - (string, Optional) The network id to attach to the network interface. It will be hex, in the format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`. diff --git a/website/source/layouts/digitalocean.erb b/website/source/layouts/digitalocean.erb index 0d91752a4c..c124499a2e 100644 --- a/website/source/layouts/digitalocean.erb +++ b/website/source/layouts/digitalocean.erb @@ -10,6 +10,15 @@ DigitalOcean Provider +