merge upstream master

This commit is contained in:
Scott Nowicki 2017-04-28 16:07:56 -05:00
commit 8784f3de93
56 changed files with 1176 additions and 230 deletions

View File

@ -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)

View File

@ -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."; \

View File

@ -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)

View File

@ -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

View File

@ -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")
}
}
}

View File

@ -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",

View File

@ -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

View File

@ -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)

View File

@ -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
-----------------------------------------------------`

View File

@ -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)

View File

@ -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" {

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)

View File

@ -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]
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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]}"
}
`

View File

@ -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 {

View File

@ -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,

View File

@ -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!`

View File

@ -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)
}
}

View File

@ -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},

View File

@ -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

View File

@ -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)}"
}

View File

@ -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.

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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) {

View File

@ -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()
}

View File

@ -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: {},

View File

@ -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()
}

10
vendor/vendor.json vendored
View File

@ -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=",

View File

@ -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).

View File

@ -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.

View File

@ -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).

4
website/source/docs/enterprise/state/pushing.html.md Executable file → Normal file
View File

@ -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"
```
```

View File

@ -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.

View File

@ -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.

View File

@ -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"
}

View File

@ -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

View File

@ -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").

View File

@ -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.
---

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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`.

View File

@ -10,6 +10,15 @@
<a href="/docs/providers/do/index.html">DigitalOcean Provider</a>
</li>
<li<%= sidebar_current("docs-do-datasource") %>>
<a href="#">Data Sources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-do-datasource-image") %>>
<a href="/docs/providers/do/d/image.html">digitalocean_image</a>
</li>
</ul>
</li>
<li<%= sidebar_current("docs-do-resource") %>>
<a href="#">Resources</a>
<ul class="nav nav-visible">

View File

@ -17,10 +17,14 @@
<a href="/docs/providers/heroku/r/addon.html">heroku_addon</a>
</li>
<li<%= sidebar_current("docs-heroku-resource-app") %>>
<li<%= sidebar_current("docs-heroku-resource-app-x") %>>
<a href="/docs/providers/heroku/r/app.html">heroku_app</a>
</li>
<li<%= sidebar_current("docs-heroku-resource-app-feature") %>>
<a href="/docs/providers/heroku/r/app_feature.html">heroku_app_feature</a>
</li>
<li<%= sidebar_current("docs-heroku-resource-cert") %>>
<a href="/docs/providers/heroku/r/cert.html">heroku_cert</a>
</li>