Merge branch 'master' into p-aws-randomize-test-names2

This commit is contained in:
Matthew Frahry 2017-04-03 11:34:16 -06:00 committed by GitHub
commit 02d3e1c12e
210 changed files with 7761 additions and 2925 deletions

4
.gitattributes vendored
View File

@ -1,4 +0,0 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
*.go eol=lf

View File

@ -1,14 +1,52 @@
## 0.9.3 (unreleased)
FEATURES:
* **New Resource:** `aws_lightsail_static_ip` [GH-13175]
* **New Resource:** `aws_lightsail_static_ip_attachment` [GH-13207]
* **New Resource:** `aws_ses_domain_identity` [GH-13098]
* **New Resource:** `kubernetes_secret` [GH-12960]
* **New Data Source:** `aws_iam_role` [GH-13213]
IMPROVEMENTS:
* backend/remote-state: Add support for assume role extensions to s3 backend [GH-13236]
* config: New interpolation functions `basename` and `dirname`, for file path manipulation [GH-13080]
* helper/resource: Allow unknown "pending" states [GH-13099]
* provider/aws: Add support to set iam_role_arn on cloudformation Stack [GH-12547]
* provider/aws: Support priority and listener_arn update of alb_listener_rule [GH-13125]
* provider/aws: Support priority and listener_arn update of alb_listener_rule [GH-13125]
* provider/aws: Deprecate roles in favour of role in iam_instance_profile [GH-13130]
* provider/aws: Make alb_target_group_attachment port optional [GH-13139]
* provider/aws: `aws_api_gateway_domain_name` `certificate_private_key` field marked as sensitive [GH-13147]
* provider/aws: `aws_directory_service_directory` `password` field marked as sensitive [GH-13147]
* provider/aws: `aws_kinesis_firehose_delivery_stream` `password` field marked as sensitive [GH-13147]
* provider/aws: `aws_opsworks_application` `app_source.0.password` & `ssl_configuration.0.private_key` fields marked as sensitive [GH-13147]
* provider/aws: `aws_opsworks_stack` `custom_cookbooks_source.0.password` field marked as sensitive [GH-13147]
* provider/aws: Support the ability to enable / disable ipv6 support in VPC [GH-12527]
* provider/aws: Added API Gateway integration update [GH-13249]
* provider/aws: Add `identifier` | `name_prefix` to RDS resources [GH-13232]
* provider/aws: Validate `aws_ecs_task_definition.container_definitions` [GH-12161]
* provider/github: Handle the case when issue labels already exist [GH-13182]
* provider/google: Mark `google_container_cluster`'s `client_key` & `password` inside `master_auth` as sensitive [GH-13148]
* provider/triton: Move to joyent/triton-go [GH-13225]
BUG FIXES:
* core: Escaped interpolation-like sequences (like `$${foo}`) now permitted in variable defaults [GH-13137]
* provider/aws: Add Support for maintenance_window and back_window to rds_cluster_instance [GH-13134]
* provider/aws: Increase timeout for AMI registration [GH-13159]
* provider/aws: Increase timeouts for ELB [GH-13161]
* provider/aws: `volume_type` of `aws_elasticsearch_domain.0.ebs_options` marked as `Computed` which prevents spurious diffs [GH-13160]
* provider/aws: Don't set DBName on `aws_db_instance` from snapshot [GH-13140]
* provider/aws: Add DiffSuppression to aws_ecs_service placement_strategies [GH-13220]
* provider/aws: Refresh aws_alb_target_group stickiness on manual updates [GH-13199]
* provider/aws: Preserve default retain_on_delete in cloudfront import [GH-13209]
* provider/aws: Refresh aws_alb_target_group tags [GH-13200]
* provider/aws: Set aws_vpn_connection to recreate when in deleted state [GH-13204]
* provider/aws: Wait for aws_opsworks_instance to be running when it's specified [GH-13218]
* provider/aws: Handle `aws_lambda_function` missing s3 key error [GH-10960]
* provider/aws: Set stickiness to computed in alb_target_group [GH-13278]
* provider/azurerm: Network Security Group - ignoring protocol casing at Import time [GH-13153]
## 0.9.2 (March 28, 2017)

View File

@ -1,4 +1,4 @@
// Code generated by "stringer -type=countHookAction hook_count_action.go"; DO NOT EDIT
// Code generated by "stringer -type=countHookAction hook_count_action.go"; DO NOT EDIT.
package local

View File

@ -1,4 +1,4 @@
// Code generated by "stringer -type=OperationType operation_type.go"; DO NOT EDIT
// Code generated by "stringer -type=OperationType operation_type.go"; DO NOT EDIT.
package backend

View File

@ -21,101 +21,122 @@ import (
func New() backend.Backend {
s := &schema.Backend{
Schema: map[string]*schema.Schema{
"bucket": &schema.Schema{
"bucket": {
Type: schema.TypeString,
Required: true,
Description: "The name of the S3 bucket",
},
"key": &schema.Schema{
"key": {
Type: schema.TypeString,
Required: true,
Description: "The path to the state file inside the bucket",
},
"region": &schema.Schema{
"region": {
Type: schema.TypeString,
Required: true,
Description: "The region of the S3 bucket.",
DefaultFunc: schema.EnvDefaultFunc("AWS_DEFAULT_REGION", nil),
},
"endpoint": &schema.Schema{
"endpoint": {
Type: schema.TypeString,
Optional: true,
Description: "A custom endpoint for the S3 API",
DefaultFunc: schema.EnvDefaultFunc("AWS_S3_ENDPOINT", ""),
},
"encrypt": &schema.Schema{
"encrypt": {
Type: schema.TypeBool,
Optional: true,
Description: "Whether to enable server side encryption of the state file",
Default: false,
},
"acl": &schema.Schema{
"acl": {
Type: schema.TypeString,
Optional: true,
Description: "Canned ACL to be applied to the state file",
Default: "",
},
"access_key": &schema.Schema{
"access_key": {
Type: schema.TypeString,
Optional: true,
Description: "AWS access key",
Default: "",
},
"secret_key": &schema.Schema{
"secret_key": {
Type: schema.TypeString,
Optional: true,
Description: "AWS secret key",
Default: "",
},
"kms_key_id": &schema.Schema{
"kms_key_id": {
Type: schema.TypeString,
Optional: true,
Description: "The ARN of a KMS Key to use for encrypting the state",
Default: "",
},
"lock_table": &schema.Schema{
"lock_table": {
Type: schema.TypeString,
Optional: true,
Description: "DynamoDB table for state locking",
Default: "",
},
"profile": &schema.Schema{
"profile": {
Type: schema.TypeString,
Optional: true,
Description: "AWS profile name",
Default: "",
},
"shared_credentials_file": &schema.Schema{
"shared_credentials_file": {
Type: schema.TypeString,
Optional: true,
Description: "Path to a shared credentials file",
Default: "",
},
"token": &schema.Schema{
"token": {
Type: schema.TypeString,
Optional: true,
Description: "MFA token",
Default: "",
},
"role_arn": &schema.Schema{
"role_arn": {
Type: schema.TypeString,
Optional: true,
Description: "The role to be assumed",
Default: "",
},
"session_name": {
Type: schema.TypeString,
Optional: true,
Description: "The session name to use when assuming the role.",
Default: "",
},
"external_id": {
Type: schema.TypeString,
Optional: true,
Description: "The external ID to use when assuming the role",
Default: "",
},
"assume_role_policy": {
Type: schema.TypeString,
Optional: true,
Description: "The permissions applied when assuming a role.",
Default: "",
},
},
}
@ -162,6 +183,9 @@ func (b *Backend) configure(ctx context.Context) error {
Profile: data.Get("profile").(string),
CredsFilename: data.Get("shared_credentials_file").(string),
AssumeRoleARN: data.Get("role_arn").(string),
AssumeRoleSessionName: data.Get("session_name").(string),
AssumeRoleExternalID: data.Get("external_id").(string),
AssumeRolePolicy: data.Get("assume_role_policy").(string),
})
if err != nil {
return err

View File

@ -0,0 +1,67 @@
package aws
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
)
func dataSourceAwsIAMRole() *schema.Resource {
return &schema.Resource{
Read: dataSourceAwsIAMRoleRead,
Schema: map[string]*schema.Schema{
"arn": {
Type: schema.TypeString,
Computed: true,
},
"assume_role_policy_document": {
Type: schema.TypeString,
Computed: true,
},
"path": {
Type: schema.TypeString,
Computed: true,
},
"role_id": {
Type: schema.TypeString,
Computed: true,
},
"role_name": {
Type: schema.TypeString,
Required: true,
},
},
}
}
func dataSourceAwsIAMRoleRead(d *schema.ResourceData, meta interface{}) error {
iamconn := meta.(*AWSClient).iamconn
roleName := d.Get("role_name").(string)
req := &iam.GetRoleInput{
RoleName: aws.String(roleName),
}
resp, err := iamconn.GetRole(req)
if err != nil {
return errwrap.Wrapf("Error getting roles: {{err}}", err)
}
if resp == nil {
return fmt.Errorf("no IAM role found")
}
role := resp.Role
d.SetId(*role.RoleId)
d.Set("arn", role.Arn)
d.Set("assume_role_policy_document", role.AssumeRolePolicyDocument)
d.Set("path", role.Path)
d.Set("role_id", role.RoleId)
return nil
}

View File

@ -0,0 +1,59 @@
package aws
import (
"regexp"
"testing"
"github.com/hashicorp/terraform/helper/resource"
)
func TestAccAWSDataSourceIAMRole_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccAwsIAMRoleConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("data.aws_iam_role.test", "role_id"),
resource.TestCheckResourceAttr("data.aws_iam_role.test", "assume_role_policy_document", "%7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%7B%22Sid%22%3A%22%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%22ec2.amazonaws.com%22%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D"),
resource.TestCheckResourceAttr("data.aws_iam_role.test", "path", "/testpath/"),
resource.TestCheckResourceAttr("data.aws_iam_role.test", "role_name", "TestRole"),
resource.TestMatchResourceAttr("data.aws_iam_role.test", "arn", regexp.MustCompile("^arn:aws:iam::[0-9]{12}:role/testpath/TestRole$")),
),
},
},
})
}
const testAccAwsIAMRoleConfig = `
provider "aws" {
region = "us-east-1"
}
resource "aws_iam_role" "test_role" {
name = "TestRole"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
path = "/testpath/"
}
data "aws_iam_role" "test" {
role_name = "${aws_iam_role.test_role.name}"
}
`

View File

@ -136,7 +136,7 @@ func dataSourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) erro
if matchingTags && matchingVPC {
if hostedZoneFound != nil {
return fmt.Errorf("multplie Route53Zone found please use vpc_id option to filter")
return fmt.Errorf("multiple Route53Zone found please use vpc_id option to filter")
} else {
hostedZoneFound = hostedZone
}

View File

@ -7,6 +7,10 @@ import (
)
func resourceAwsCloudFrontDistributionImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
// This is a non API attribute
// We are merely setting this to the same value as the Default setting in the schema
d.Set("retain_on_delete", false)
conn := meta.(*AWSClient).cloudfrontconn
id := d.Id()
resp, err := conn.GetDistributionConfig(&cloudfront.GetDistributionConfigInput{

View File

@ -19,16 +19,13 @@ func TestAccAWSCloudFrontDistribution_importBasic(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudFrontDistributionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testConfig,
},
resource.TestStep{
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
// Ignore retain_on_delete since it doesn't come from the AWS
// API.
ImportStateVerifyIgnore: []string{"retain_on_delete"},
},
},
})

View File

@ -3,11 +3,13 @@ package aws
import (
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
)
func TestAccAWSEFSFileSystem_importBasic(t *testing.T) {
resourceName := "aws_efs_file_system.foo-with-tags"
rInt := acctest.RandInt()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@ -15,7 +17,7 @@ func TestAccAWSEFSFileSystem_importBasic(t *testing.T) {
CheckDestroy: testAccCheckEfsFileSystemDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSEFSFileSystemConfigWithTags,
Config: testAccAWSEFSFileSystemConfigWithTags(rInt),
},
resource.TestStep{

View File

@ -174,6 +174,7 @@ func Provider() terraform.ResourceProvider {
"aws_elb_service_account": dataSourceAwsElbServiceAccount(),
"aws_iam_account_alias": dataSourceAwsIamAccountAlias(),
"aws_iam_policy_document": dataSourceAwsIamPolicyDocument(),
"aws_iam_role": dataSourceAwsIAMRole(),
"aws_iam_server_certificate": dataSourceAwsIAMServerCertificate(),
"aws_instance": dataSourceAwsInstance(),
"aws_ip_ranges": dataSourceAwsIPRanges(),
@ -337,6 +338,8 @@ func Provider() terraform.ResourceProvider {
"aws_lightsail_domain": resourceAwsLightsailDomain(),
"aws_lightsail_instance": resourceAwsLightsailInstance(),
"aws_lightsail_key_pair": resourceAwsLightsailKeyPair(),
"aws_lightsail_static_ip": resourceAwsLightsailStaticIp(),
"aws_lightsail_static_ip_attachment": resourceAwsLightsailStaticIpAttachment(),
"aws_lb_cookie_stickiness_policy": resourceAwsLBCookieStickinessPolicy(),
"aws_load_balancer_policy": resourceAwsLoadBalancerPolicy(),
"aws_load_balancer_backend_server_policy": resourceAwsLoadBalancerBackendServerPolicies(),
@ -383,6 +386,7 @@ func Provider() terraform.ResourceProvider {
"aws_route_table": resourceAwsRouteTable(),
"aws_route_table_association": resourceAwsRouteTableAssociation(),
"aws_ses_active_receipt_rule_set": resourceAwsSesActiveReceiptRuleSet(),
"aws_ses_domain_identity": resourceAwsSesDomainIdentity(),
"aws_ses_receipt_filter": resourceAwsSesReceiptFilter(),
"aws_ses_receipt_rule": resourceAwsSesReceiptRule(),
"aws_ses_receipt_rule_set": resourceAwsSesReceiptRuleSet(),

View File

@ -73,6 +73,7 @@ func resourceAwsAlbTargetGroup() *schema.Resource {
"stickiness": {
Type: schema.TypeList,
Optional: true,
Computed: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
@ -258,11 +259,19 @@ func resourceAwsAlbTargetGroupRead(d *schema.ResourceData, meta interface{}) err
for _, attr := range attrResp.Attributes {
switch *attr.Key {
case "stickiness.enabled":
stickinessMap["enabled"] = *attr.Value
enabled, err := strconv.ParseBool(*attr.Value)
if err != nil {
return fmt.Errorf("Error converting stickiness.enabled to bool: %s", *attr.Value)
}
stickinessMap["enabled"] = enabled
case "stickiness.type":
stickinessMap["type"] = *attr.Value
case "stickiness.lb_cookie.duration_seconds":
stickinessMap["cookie_duration"] = *attr.Value
duration, err := strconv.Atoi(*attr.Value)
if err != nil {
return fmt.Errorf("Error converting stickiness.lb_cookie.duration_seconds to int: %s", *attr.Value)
}
stickinessMap["cookie_duration"] = duration
case "deregistration_delay.timeout_seconds":
timeout, err := strconv.Atoi(*attr.Value)
if err != nil {
@ -271,7 +280,24 @@ func resourceAwsAlbTargetGroupRead(d *schema.ResourceData, meta interface{}) err
d.Set("deregistration_delay", timeout)
}
}
d.Set("stickiness", []interface{}{stickinessMap})
if err := d.Set("stickiness", []interface{}{stickinessMap}); err != nil {
return err
}
tagsResp, err := elbconn.DescribeTags(&elbv2.DescribeTagsInput{
ResourceArns: []*string{aws.String(d.Id())},
})
if err != nil {
return errwrap.Wrapf("Error retrieving Target Group Tags: {{err}}", err)
}
for _, t := range tagsResp.TagDescriptions {
if *t.ResourceArn == d.Id() {
if err := d.Set("tags", tagsToMapELBv2(t.Tags)); err != nil {
return err
}
}
}
return nil
}

View File

@ -34,7 +34,7 @@ func resourceAwsAlbTargetGroupAttachment() *schema.Resource {
"port": {
Type: schema.TypeInt,
ForceNew: true,
Required: true,
Optional: true,
},
},
}
@ -43,18 +43,21 @@ func resourceAwsAlbTargetGroupAttachment() *schema.Resource {
func resourceAwsAlbAttachmentCreate(d *schema.ResourceData, meta interface{}) error {
elbconn := meta.(*AWSClient).elbv2conn
params := &elbv2.RegisterTargetsInput{
TargetGroupArn: aws.String(d.Get("target_group_arn").(string)),
Targets: []*elbv2.TargetDescription{
{
target := &elbv2.TargetDescription{
Id: aws.String(d.Get("target_id").(string)),
Port: aws.Int64(int64(d.Get("port").(int))),
},
},
}
log.Printf("[INFO] Registering Target %s (%d) with Target Group %s", d.Get("target_id").(string),
d.Get("port").(int), d.Get("target_group_arn").(string))
if v, ok := d.GetOk("port"); ok {
target.Port = aws.Int64(int64(v.(int)))
}
params := &elbv2.RegisterTargetsInput{
TargetGroupArn: aws.String(d.Get("target_group_arn").(string)),
Targets: []*elbv2.TargetDescription{target},
}
log.Printf("[INFO] Registering Target %s with Target Group %s", d.Get("target_id").(string),
d.Get("target_group_arn").(string))
_, err := elbconn.RegisterTargets(params)
if err != nil {
@ -69,14 +72,17 @@ func resourceAwsAlbAttachmentCreate(d *schema.ResourceData, meta interface{}) er
func resourceAwsAlbAttachmentDelete(d *schema.ResourceData, meta interface{}) error {
elbconn := meta.(*AWSClient).elbv2conn
target := &elbv2.TargetDescription{
Id: aws.String(d.Get("target_id").(string)),
}
if v, ok := d.GetOk("port"); ok {
target.Port = aws.Int64(int64(v.(int)))
}
params := &elbv2.DeregisterTargetsInput{
TargetGroupArn: aws.String(d.Get("target_group_arn").(string)),
Targets: []*elbv2.TargetDescription{
{
Id: aws.String(d.Get("target_id").(string)),
Port: aws.Int64(int64(d.Get("port").(int))),
},
},
Targets: []*elbv2.TargetDescription{target},
}
_, err := elbconn.DeregisterTargets(params)
@ -93,14 +99,18 @@ func resourceAwsAlbAttachmentDelete(d *schema.ResourceData, meta interface{}) er
// target, so there is no work to do beyond ensuring that the target and group still exist.
func resourceAwsAlbAttachmentRead(d *schema.ResourceData, meta interface{}) error {
elbconn := meta.(*AWSClient).elbv2conn
target := &elbv2.TargetDescription{
Id: aws.String(d.Get("target_id").(string)),
}
if v, ok := d.GetOk("port"); ok {
target.Port = aws.Int64(int64(v.(int)))
}
resp, err := elbconn.DescribeTargetHealth(&elbv2.DescribeTargetHealthInput{
TargetGroupArn: aws.String(d.Get("target_group_arn").(string)),
Targets: []*elbv2.TargetDescription{
{
Id: aws.String(d.Get("target_id").(string)),
Port: aws.Int64(int64(d.Get("port").(int))),
},
},
Targets: []*elbv2.TargetDescription{target},
})
if err != nil {
if isTargetGroupNotFound(err) {

View File

@ -3,14 +3,15 @@ package aws
import (
"errors"
"fmt"
"strconv"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/elbv2"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"strconv"
"testing"
)
func TestAccAWSALBTargetGroupAttachment_basic(t *testing.T) {
@ -32,6 +33,25 @@ func TestAccAWSALBTargetGroupAttachment_basic(t *testing.T) {
})
}
func TestAccAWSALBTargetGroupAttachment_withoutPort(t *testing.T) {
targetGroupName := fmt.Sprintf("test-target-group-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "aws_alb_target_group.test",
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSALBTargetGroupAttachmentDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSALBTargetGroupAttachmentConfigWithoutPort(targetGroupName),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSALBTargetGroupAttachmentExists("aws_alb_target_group_attachment.test"),
),
},
},
})
}
func testAccCheckAWSALBTargetGroupAttachmentExists(n string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
@ -45,15 +65,20 @@ func testAccCheckAWSALBTargetGroupAttachmentExists(n string) resource.TestCheckF
conn := testAccProvider.Meta().(*AWSClient).elbv2conn
port, _ := strconv.Atoi(rs.Primary.Attributes["port"])
describe, err := conn.DescribeTargetHealth(&elbv2.DescribeTargetHealthInput{
TargetGroupArn: aws.String(rs.Primary.Attributes["target_group_arn"]),
Targets: []*elbv2.TargetDescription{
{
_, hasPort := rs.Primary.Attributes["port"]
targetGroupArn, _ := rs.Primary.Attributes["target_group_arn"]
target := &elbv2.TargetDescription{
Id: aws.String(rs.Primary.Attributes["target_id"]),
Port: aws.Int64(int64(port)),
},
},
}
if hasPort == true {
port, _ := strconv.Atoi(rs.Primary.Attributes["port"])
target.Port = aws.Int64(int64(port))
}
describe, err := conn.DescribeTargetHealth(&elbv2.DescribeTargetHealthInput{
TargetGroupArn: aws.String(targetGroupArn),
Targets: []*elbv2.TargetDescription{target},
})
if err != nil {
@ -76,15 +101,20 @@ func testAccCheckAWSALBTargetGroupAttachmentDestroy(s *terraform.State) error {
continue
}
port, _ := strconv.Atoi(rs.Primary.Attributes["port"])
describe, err := conn.DescribeTargetHealth(&elbv2.DescribeTargetHealthInput{
TargetGroupArn: aws.String(rs.Primary.Attributes["target_group_arn"]),
Targets: []*elbv2.TargetDescription{
{
_, hasPort := rs.Primary.Attributes["port"]
targetGroupArn, _ := rs.Primary.Attributes["target_group_arn"]
target := &elbv2.TargetDescription{
Id: aws.String(rs.Primary.Attributes["target_id"]),
Port: aws.Int64(int64(port)),
},
},
}
if hasPort == true {
port, _ := strconv.Atoi(rs.Primary.Attributes["port"])
target.Port = aws.Int64(int64(port))
}
describe, err := conn.DescribeTargetHealth(&elbv2.DescribeTargetHealthInput{
TargetGroupArn: aws.String(targetGroupArn),
Targets: []*elbv2.TargetDescription{target},
})
if err == nil {
if len(describe.TargetHealthDescriptions) != 0 {
@ -103,6 +133,55 @@ func testAccCheckAWSALBTargetGroupAttachmentDestroy(s *terraform.State) error {
return nil
}
func testAccAWSALBTargetGroupAttachmentConfigWithoutPort(targetGroupName string) string {
return fmt.Sprintf(`
resource "aws_alb_target_group_attachment" "test" {
target_group_arn = "${aws_alb_target_group.test.arn}"
target_id = "${aws_instance.test.id}"
}
resource "aws_instance" "test" {
ami = "ami-f701cb97"
instance_type = "t2.micro"
subnet_id = "${aws_subnet.subnet.id}"
}
resource "aws_alb_target_group" "test" {
name = "%s"
port = 443
protocol = "HTTPS"
vpc_id = "${aws_vpc.test.id}"
deregistration_delay = 200
stickiness {
type = "lb_cookie"
cookie_duration = 10000
}
health_check {
path = "/health"
interval = 60
port = 8081
protocol = "HTTP"
timeout = 3
healthy_threshold = 3
unhealthy_threshold = 3
matcher = "200-299"
}
}
resource "aws_subnet" "subnet" {
cidr_block = "10.0.1.0/24"
vpc_id = "${aws_vpc.test.id}"
}
resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"
}`, targetGroupName)
}
func testAccAWSALBTargetGroupAttachmentConfig_basic(targetGroupName string) string {
return fmt.Sprintf(`
resource "aws_alb_target_group_attachment" "test" {

View File

@ -77,6 +77,8 @@ func TestAccAWSALBTargetGroup_basic(t *testing.T) {
resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.healthy_threshold", "3"),
resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.unhealthy_threshold", "3"),
resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.matcher", "200-299"),
resource.TestCheckResourceAttr("aws_alb_target_group.test", "tags.%", "1"),
resource.TestCheckResourceAttr("aws_alb_target_group.test", "tags.TestName", "TestAccAWSALBTargetGroup_basic"),
),
},
},

View File

@ -18,7 +18,7 @@ import (
)
const (
AWSAMIRetryTimeout = 10 * time.Minute
AWSAMIRetryTimeout = 20 * time.Minute
AWSAMIDeleteRetryTimeout = 20 * time.Minute
AWSAMIRetryDelay = 5 * time.Second
AWSAMIRetryMinTimeout = 3 * time.Second

View File

@ -48,6 +48,7 @@ func resourceAwsApiGatewayDomainName() *schema.Resource {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
Sensitive: true,
ConflictsWith: []string{"certificate_arn"},
},

View File

@ -11,87 +11,94 @@ import (
"github.com/aws/aws-sdk-go/service/apigateway"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"strings"
)
func resourceAwsApiGatewayIntegration() *schema.Resource {
return &schema.Resource{
Create: resourceAwsApiGatewayIntegrationCreate,
Read: resourceAwsApiGatewayIntegrationRead,
Update: resourceAwsApiGatewayIntegrationCreate,
Update: resourceAwsApiGatewayIntegrationUpdate,
Delete: resourceAwsApiGatewayIntegrationDelete,
Schema: map[string]*schema.Schema{
"rest_api_id": &schema.Schema{
"rest_api_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"resource_id": &schema.Schema{
"resource_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"http_method": &schema.Schema{
"http_method": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateHTTPMethod,
},
"type": &schema.Schema{
"type": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateApiGatewayIntegrationType,
},
"uri": &schema.Schema{
"uri": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"credentials": &schema.Schema{
"credentials": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"integration_http_method": &schema.Schema{
"integration_http_method": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validateHTTPMethod,
},
"request_templates": &schema.Schema{
"request_templates": {
Type: schema.TypeMap,
Optional: true,
Elem: schema.TypeString,
},
"request_parameters": &schema.Schema{
"request_parameters": {
Type: schema.TypeMap,
Elem: schema.TypeString,
Optional: true,
ConflictsWith: []string{"request_parameters_in_json"},
},
"request_parameters_in_json": &schema.Schema{
"request_parameters_in_json": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"request_parameters"},
Deprecated: "Use field request_parameters instead",
},
"content_handling": &schema.Schema{
"content_handling": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validateApiGatewayIntegrationContentHandling,
},
"passthrough_behavior": &schema.Schema{
"passthrough_behavior": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validateApiGatewayIntegrationPassthroughBehavior,
},
},
@ -101,6 +108,7 @@ func resourceAwsApiGatewayIntegration() *schema.Resource {
func resourceAwsApiGatewayIntegrationCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).apigateway
log.Print("[DEBUG] Creating API Gateway Integration")
var integrationHttpMethod *string
if v, ok := d.GetOk("integration_http_method"); ok {
integrationHttpMethod = aws.String(v.(string))
@ -163,13 +171,13 @@ func resourceAwsApiGatewayIntegrationCreate(d *schema.ResourceData, meta interfa
d.SetId(fmt.Sprintf("agi-%s-%s-%s", d.Get("rest_api_id").(string), d.Get("resource_id").(string), d.Get("http_method").(string)))
return nil
return resourceAwsApiGatewayIntegrationRead(d, meta)
}
func resourceAwsApiGatewayIntegrationRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).apigateway
log.Printf("[DEBUG] Reading API Gateway Integration %s", d.Id())
log.Printf("[DEBUG] Reading API Gateway Integration: %s", d.Id())
integration, err := conn.GetIntegration(&apigateway.GetIntegrationInput{
HttpMethod: aws.String(d.Get("http_method").(string)),
ResourceId: aws.String(d.Get("resource_id").(string)),
@ -191,17 +199,127 @@ func resourceAwsApiGatewayIntegrationRead(d *schema.ResourceData, meta interface
}
d.Set("request_templates", aws.StringValueMap(integration.RequestTemplates))
d.Set("credentials", integration.Credentials)
d.Set("type", integration.Type)
d.Set("uri", integration.Uri)
d.Set("request_parameters", aws.StringValueMap(integration.RequestParameters))
d.Set("request_parameters_in_json", aws.StringValueMap(integration.RequestParameters))
d.Set("passthrough_behavior", integration.PassthroughBehavior)
if integration.Uri != nil {
d.Set("uri", integration.Uri)
}
if integration.Credentials != nil {
d.Set("credentials", integration.Credentials)
}
if integration.ContentHandling != nil {
d.Set("content_handling", integration.ContentHandling)
}
return nil
}
func resourceAwsApiGatewayIntegrationUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).apigateway
log.Printf("[DEBUG] Updating API Gateway Integration: %s", d.Id())
operations := make([]*apigateway.PatchOperation, 0)
// https://docs.aws.amazon.com/apigateway/api-reference/link-relation/integration-update/#remarks
// According to the above documentation, only a few parts are addable / removable.
if d.HasChange("request_templates") {
o, n := d.GetChange("request_templates")
prefix := "requestTemplates"
os := o.(map[string]interface{})
ns := n.(map[string]interface{})
// Handle Removal
for k := range os {
if _, ok := ns[k]; !ok {
operations = append(operations, &apigateway.PatchOperation{
Op: aws.String("remove"),
Path: aws.String(fmt.Sprintf("/%s/%s", prefix, strings.Replace(k, "/", "~1", -1))),
})
}
}
for k, v := range ns {
// Handle replaces
if _, ok := os[k]; ok {
operations = append(operations, &apigateway.PatchOperation{
Op: aws.String("replace"),
Path: aws.String(fmt.Sprintf("/%s/%s", prefix, strings.Replace(k, "/", "~1", -1))),
Value: aws.String(v.(string)),
})
}
// Handle additions
if _, ok := os[k]; !ok {
operations = append(operations, &apigateway.PatchOperation{
Op: aws.String("add"),
Path: aws.String(fmt.Sprintf("/%s/%s", prefix, strings.Replace(k, "/", "~1", -1))),
Value: aws.String(v.(string)),
})
}
}
}
if d.HasChange("request_parameters") {
o, n := d.GetChange("request_parameters")
prefix := "requestParameters"
os := o.(map[string]interface{})
ns := n.(map[string]interface{})
// Handle Removal
for k := range os {
if _, ok := ns[k]; !ok {
operations = append(operations, &apigateway.PatchOperation{
Op: aws.String("remove"),
Path: aws.String(fmt.Sprintf("/%s/%s", prefix, strings.Replace(k, "/", "~1", -1))),
})
}
}
for k, v := range ns {
// Handle replaces
if _, ok := os[k]; ok {
operations = append(operations, &apigateway.PatchOperation{
Op: aws.String("replace"),
Path: aws.String(fmt.Sprintf("/%s/%s", prefix, strings.Replace(k, "/", "~1", -1))),
Value: aws.String(v.(string)),
})
}
// Handle additions
if _, ok := os[k]; !ok {
operations = append(operations, &apigateway.PatchOperation{
Op: aws.String("add"),
Path: aws.String(fmt.Sprintf("/%s/%s", prefix, strings.Replace(k, "/", "~1", -1))),
Value: aws.String(v.(string)),
})
}
}
}
params := &apigateway.UpdateIntegrationInput{
HttpMethod: aws.String(d.Get("http_method").(string)),
ResourceId: aws.String(d.Get("resource_id").(string)),
RestApiId: aws.String(d.Get("rest_api_id").(string)),
PatchOperations: operations,
}
_, err := conn.UpdateIntegration(params)
if err != nil {
return fmt.Errorf("Error updating API Gateway Integration: %s", err)
}
d.SetId(fmt.Sprintf("agi-%s-%s-%s", d.Get("rest_api_id").(string), d.Get("resource_id").(string), d.Get("http_method").(string)))
return resourceAwsApiGatewayIntegrationRead(d, meta)
}
func resourceAwsApiGatewayIntegrationDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).apigateway
log.Printf("[DEBUG] Deleting API Gateway Integration: %s", d.Id())

View File

@ -19,88 +19,80 @@ func TestAccAWSAPIGatewayIntegration_basic(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSAPIGatewayIntegrationDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSAPIGatewayIntegrationConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSAPIGatewayIntegrationExists("aws_api_gateway_integration.test", &conf),
testAccCheckAWSAPIGatewayIntegrationAttributes(&conf),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration.test", "type", "HTTP"),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration.test", "integration_http_method", "GET"),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration.test", "uri", "https://www.google.de"),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration.test", "request_templates.application/json", ""),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration.test", "request_templates.application/xml", "#set($inputRoot = $input.path('$'))\n{ }"),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration.test", "passthrough_behavior", "WHEN_NO_MATCH"),
resource.TestCheckNoResourceAttr(
"aws_api_gateway_integration.test", "content_handling"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "type", "HTTP"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "integration_http_method", "GET"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "uri", "https://www.google.de"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "passthrough_behavior", "WHEN_NO_MATCH"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "content_handling", "CONVERT_TO_TEXT"),
resource.TestCheckNoResourceAttr("aws_api_gateway_integration.test", "credentials"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_parameters.%", "2"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_parameters.integration.request.header.X-Authorization", "'static'"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_parameters.integration.request.header.X-Foo", "'Bar'"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_templates.%", "2"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_templates.application/json", ""),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_templates.application/xml", "#set($inputRoot = $input.path('$'))\n{ }"),
),
},
resource.TestStep{
{
Config: testAccAWSAPIGatewayIntegrationConfigUpdate,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSAPIGatewayIntegrationExists("aws_api_gateway_integration.test", &conf),
testAccCheckAWSAPIGatewayMockIntegrationAttributes(&conf),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration.test", "type", "MOCK"),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration.test", "integration_http_method", ""),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration.test", "uri", ""),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration.test", "passthrough_behavior", "NEVER"),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration.test", "content_handling", "CONVERT_TO_BINARY"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "type", "HTTP"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "integration_http_method", "GET"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "uri", "https://www.google.de"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "passthrough_behavior", "WHEN_NO_MATCH"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "content_handling", "CONVERT_TO_TEXT"),
resource.TestCheckNoResourceAttr("aws_api_gateway_integration.test", "credentials"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_parameters.%", "2"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_parameters.integration.request.header.X-Authorization", "'updated'"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_parameters.integration.request.header.X-FooBar", "'Baz'"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_templates.%", "2"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_templates.application/json", "{'foobar': 'bar}"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_templates.text/html", "<html>Foo</html>"),
),
},
{
Config: testAccAWSAPIGatewayIntegrationConfigUpdateNoTemplates,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSAPIGatewayIntegrationExists("aws_api_gateway_integration.test", &conf),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "type", "HTTP"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "integration_http_method", "GET"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "uri", "https://www.google.de"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "passthrough_behavior", "WHEN_NO_MATCH"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "content_handling", "CONVERT_TO_TEXT"),
resource.TestCheckNoResourceAttr("aws_api_gateway_integration.test", "credentials"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_parameters.%", "0"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_templates.%", "0"),
),
},
{
Config: testAccAWSAPIGatewayIntegrationConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSAPIGatewayIntegrationExists("aws_api_gateway_integration.test", &conf),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "type", "HTTP"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "integration_http_method", "GET"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "uri", "https://www.google.de"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "passthrough_behavior", "WHEN_NO_MATCH"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "content_handling", "CONVERT_TO_TEXT"),
resource.TestCheckNoResourceAttr("aws_api_gateway_integration.test", "credentials"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_parameters.%", "2"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_parameters.integration.request.header.X-Authorization", "'static'"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_templates.%", "2"),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_templates.application/json", ""),
resource.TestCheckResourceAttr("aws_api_gateway_integration.test", "request_templates.application/xml", "#set($inputRoot = $input.path('$'))\n{ }"),
),
},
},
})
}
func testAccCheckAWSAPIGatewayMockIntegrationAttributes(conf *apigateway.Integration) resource.TestCheckFunc {
return func(s *terraform.State) error {
if *conf.Type != "MOCK" {
return fmt.Errorf("Wrong Type: %q", *conf.Type)
}
if *conf.RequestParameters["integration.request.header.X-Authorization"] != "'updated'" {
return fmt.Errorf("wrong updated RequestParameters for header.X-Authorization")
}
if *conf.ContentHandling != "CONVERT_TO_BINARY" {
return fmt.Errorf("wrong ContentHandling: %q", *conf.ContentHandling)
}
return nil
}
}
func testAccCheckAWSAPIGatewayIntegrationAttributes(conf *apigateway.Integration) resource.TestCheckFunc {
return func(s *terraform.State) error {
if *conf.HttpMethod == "" {
return fmt.Errorf("empty HttpMethod")
}
if *conf.Uri != "https://www.google.de" {
return fmt.Errorf("wrong Uri")
}
if *conf.Type != "HTTP" {
return fmt.Errorf("wrong Type")
}
if conf.RequestTemplates["application/json"] != nil {
return fmt.Errorf("wrong RequestTemplate for application/json")
}
if *conf.RequestTemplates["application/xml"] != "#set($inputRoot = $input.path('$'))\n{ }" {
return fmt.Errorf("wrong RequestTemplate for application/xml")
}
if *conf.RequestParameters["integration.request.header.X-Authorization"] != "'static'" {
return fmt.Errorf("wrong RequestParameters for header.X-Authorization")
}
return nil
}
}
func testAccCheckAWSAPIGatewayIntegrationExists(n string, res *apigateway.Integration) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
@ -197,12 +189,14 @@ resource "aws_api_gateway_integration" "test" {
request_parameters = {
"integration.request.header.X-Authorization" = "'static'"
"integration.request.header.X-Foo" = "'Bar'"
}
type = "HTTP"
uri = "https://www.google.de"
integration_http_method = "GET"
passthrough_behavior = "WHEN_NO_MATCH"
content_handling = "CONVERT_TO_TEXT"
}
`
@ -233,13 +227,55 @@ resource "aws_api_gateway_integration" "test" {
resource_id = "${aws_api_gateway_resource.test.id}"
http_method = "${aws_api_gateway_method.test.http_method}"
request_templates = {
"application/json" = "{'foobar': 'bar}"
"text/html" = "<html>Foo</html>"
}
request_parameters = {
"integration.request.header.X-Authorization" = "'updated'"
"integration.request.header.X-FooBar" = "'Baz'"
}
type = "MOCK"
passthrough_behavior = "NEVER"
content_handling = "CONVERT_TO_BINARY"
type = "HTTP"
uri = "https://www.google.de"
integration_http_method = "GET"
passthrough_behavior = "WHEN_NO_MATCH"
content_handling = "CONVERT_TO_TEXT"
}
`
const testAccAWSAPIGatewayIntegrationConfigUpdateNoTemplates = `
resource "aws_api_gateway_rest_api" "test" {
name = "test"
}
resource "aws_api_gateway_resource" "test" {
rest_api_id = "${aws_api_gateway_rest_api.test.id}"
parent_id = "${aws_api_gateway_rest_api.test.root_resource_id}"
path_part = "test"
}
resource "aws_api_gateway_method" "test" {
rest_api_id = "${aws_api_gateway_rest_api.test.id}"
resource_id = "${aws_api_gateway_resource.test.id}"
http_method = "GET"
authorization = "NONE"
request_models = {
"application/json" = "Error"
}
}
resource "aws_api_gateway_integration" "test" {
rest_api_id = "${aws_api_gateway_rest_api.test.id}"
resource_id = "${aws_api_gateway_resource.test.id}"
http_method = "${aws_api_gateway_method.test.http_method}"
type = "HTTP"
uri = "https://www.google.de"
integration_http_method = "GET"
passthrough_behavior = "WHEN_NO_MATCH"
content_handling = "CONVERT_TO_TEXT"
}
`

View File

@ -75,6 +75,7 @@ func TestAccAWSCustomerGateway_disappears(t *testing.T) {
rInt := acctest.RandInt()
rBgpAsn := acctest.RandIntRange(64512, 65534)
var gateway ec2.CustomerGateway
randInt := acctest.RandInt()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,

View File

@ -105,7 +105,15 @@ func resourceAwsDbInstance() *schema.Resource {
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validateRdsId,
ConflictsWith: []string{"identifier_prefix"},
ValidateFunc: validateRdsIdentifier,
},
"identifier_prefix": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validateRdsIdentifierPrefix,
},
"instance_class": {
@ -336,10 +344,16 @@ func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error
conn := meta.(*AWSClient).rdsconn
tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{}))
identifier := d.Get("identifier").(string)
// Generate a unique ID for the user
if identifier == "" {
identifier = resource.PrefixedUniqueId("tf-")
var identifier string
if v, ok := d.GetOk("identifier"); ok {
identifier = v.(string)
} else {
if v, ok := d.GetOk("identifier_prefix"); ok {
identifier = resource.PrefixedUniqueId(v.(string))
} else {
identifier = resource.UniqueId()
}
// SQL Server identifier size is max 15 chars, so truncate
if engine := d.Get("engine").(string); engine != "" {
if strings.Contains(strings.ToLower(engine), "sqlserver") {
@ -407,8 +421,15 @@ func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error
}
if attr, ok := d.GetOk("name"); ok {
// "Note: This parameter [DBName] doesn't apply to the MySQL, PostgreSQL, or MariaDB engines."
// https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_RestoreDBInstanceFromDBSnapshot.html
switch strings.ToLower(d.Get("engine").(string)) {
case "mysql", "postgres", "mariadb":
// skip
default:
opts.DBName = aws.String(attr.(string))
}
}
if attr, ok := d.GetOk("availability_zone"); ok {
opts.AvailabilityZone = aws.String(attr.(string))

View File

@ -53,6 +53,46 @@ func TestAccAWSDBInstance_basic(t *testing.T) {
})
}
func TestAccAWSDBInstance_namePrefix(t *testing.T) {
var v rds.DBInstance
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSDBInstanceConfig_namePrefix,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.test", &v),
testAccCheckAWSDBInstanceAttributes(&v),
resource.TestMatchResourceAttr(
"aws_db_instance.test", "identifier", regexp.MustCompile("^tf-test-")),
),
},
},
})
}
func TestAccAWSDBInstance_generatedName(t *testing.T) {
var v rds.DBInstance
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSDBInstanceConfig_generatedName,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.test", &v),
testAccCheckAWSDBInstanceAttributes(&v),
),
},
},
})
}
func TestAccAWSDBInstance_kmsKey(t *testing.T) {
var v rds.DBInstance
keyRegex := regexp.MustCompile("^arn:aws:kms:")
@ -628,6 +668,37 @@ resource "aws_db_instance" "bar" {
}
}`
const testAccAWSDBInstanceConfig_namePrefix = `
resource "aws_db_instance" "test" {
allocated_storage = 10
engine = "MySQL"
identifier_prefix = "tf-test-"
instance_class = "db.t1.micro"
password = "password"
username = "root"
publicly_accessible = true
skip_final_snapshot = true
timeouts {
create = "30m"
}
}`
const testAccAWSDBInstanceConfig_generatedName = `
resource "aws_db_instance" "test" {
allocated_storage = 10
engine = "MySQL"
instance_class = "db.t1.micro"
password = "password"
username = "root"
publicly_accessible = true
skip_final_snapshot = true
timeouts {
create = "30m"
}
}`
var testAccAWSDBInstanceConfigKmsKeyId = `
resource "aws_kms_key" "foo" {
description = "Terraform acc test %s"

View File

@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"log"
"regexp"
"time"
"github.com/aws/aws-sdk-go/aws"
@ -32,10 +31,19 @@ func resourceAwsDbOptionGroup() *schema.Resource {
},
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
Required: true,
ConflictsWith: []string{"name_prefix"},
ValidateFunc: validateDbOptionGroupName,
},
"name_prefix": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validateDbOptionGroupNamePrefix,
},
"engine_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
@ -48,8 +56,9 @@ func resourceAwsDbOptionGroup() *schema.Resource {
},
"option_group_description": &schema.Schema{
Type: schema.TypeString,
Required: true,
Optional: true,
ForceNew: true,
Default: "Managed by Terraform",
},
"option": &schema.Schema{
@ -107,11 +116,20 @@ func resourceAwsDbOptionGroupCreate(d *schema.ResourceData, meta interface{}) er
rdsconn := meta.(*AWSClient).rdsconn
tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{}))
var groupName string
if v, ok := d.GetOk("name"); ok {
groupName = v.(string)
} else if v, ok := d.GetOk("name_prefix"); ok {
groupName = resource.PrefixedUniqueId(v.(string))
} else {
groupName = resource.UniqueId()
}
createOpts := &rds.CreateOptionGroupInput{
EngineName: aws.String(d.Get("engine_name").(string)),
MajorEngineVersion: aws.String(d.Get("major_engine_version").(string)),
OptionGroupDescription: aws.String(d.Get("option_group_description").(string)),
OptionGroupName: aws.String(d.Get("name").(string)),
OptionGroupName: aws.String(groupName),
Tags: tags,
}
@ -121,7 +139,7 @@ func resourceAwsDbOptionGroupCreate(d *schema.ResourceData, meta interface{}) er
return fmt.Errorf("Error creating DB Option Group: %s", err)
}
d.SetId(d.Get("name").(string))
d.SetId(groupName)
log.Printf("[INFO] DB Option Group ID: %s", d.Id())
return resourceAwsDbOptionGroupUpdate(d, meta)
@ -343,28 +361,3 @@ func buildRDSOptionGroupARN(identifier, partition, accountid, region string) (st
arn := fmt.Sprintf("arn:%s:rds:%s:%s:og:%s", partition, region, accountid, identifier)
return arn, nil
}
func validateDbOptionGroupName(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if !regexp.MustCompile(`^[a-z]`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"first character of %q must be a letter", k))
}
if !regexp.MustCompile(`^[0-9A-Za-z-]+$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"only alphanumeric characters and hyphens allowed in %q", k))
}
if regexp.MustCompile(`--`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q cannot contain two consecutive hyphens", k))
}
if regexp.MustCompile(`-$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q cannot end with a hyphen", k))
}
if len(value) > 255 {
errors = append(errors, fmt.Errorf(
"%q cannot be greater than 255 characters", k))
}
return
}

View File

@ -2,6 +2,7 @@ package aws
import (
"fmt"
"regexp"
"testing"
"github.com/aws/aws-sdk-go/aws"
@ -34,6 +35,66 @@ func TestAccAWSDBOptionGroup_basic(t *testing.T) {
})
}
func TestAccAWSDBOptionGroup_namePrefix(t *testing.T) {
var v rds.OptionGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBOptionGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSDBOptionGroup_namePrefix,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBOptionGroupExists("aws_db_option_group.test", &v),
testAccCheckAWSDBOptionGroupAttributes(&v),
resource.TestMatchResourceAttr(
"aws_db_option_group.test", "name", regexp.MustCompile("^tf-test-")),
),
},
},
})
}
func TestAccAWSDBOptionGroup_generatedName(t *testing.T) {
var v rds.OptionGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBOptionGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSDBOptionGroup_generatedName,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBOptionGroupExists("aws_db_option_group.test", &v),
testAccCheckAWSDBOptionGroupAttributes(&v),
),
},
},
})
}
func TestAccAWSDBOptionGroup_defaultDescription(t *testing.T) {
var v rds.OptionGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBOptionGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSDBOptionGroup_defaultDescription(acctest.RandInt()),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBOptionGroupExists("aws_db_option_group.test", &v),
resource.TestCheckResourceAttr(
"aws_db_option_group.test", "option_group_description", "Managed by Terraform"),
),
},
},
})
}
func TestAccAWSDBOptionGroup_basicDestroyWithInstance(t *testing.T) {
rName := fmt.Sprintf("option-group-test-terraform-%s", acctest.RandString(5))
@ -160,42 +221,6 @@ func testAccCheckAWSDBOptionGroupAttributes(v *rds.OptionGroup) resource.TestChe
}
}
func TestResourceAWSDBOptionGroupName_validation(t *testing.T) {
cases := []struct {
Value string
ErrCount int
}{
{
Value: "testing123!",
ErrCount: 1,
},
{
Value: "1testing123",
ErrCount: 1,
},
{
Value: "testing--123",
ErrCount: 1,
},
{
Value: "testing123-",
ErrCount: 1,
},
{
Value: randomString(256),
ErrCount: 1,
},
}
for _, tc := range cases {
_, errors := validateDbOptionGroupName(tc.Value, "aws_db_option_group_name")
if len(errors) != tc.ErrCount {
t.Fatalf("Expected the DB Option Group Name to trigger a validation error")
}
}
}
func testAccCheckAWSDBOptionGroupExists(n string, v *rds.OptionGroup) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
@ -387,3 +412,30 @@ resource "aws_db_option_group" "bar" {
}
`, r)
}
const testAccAWSDBOptionGroup_namePrefix = `
resource "aws_db_option_group" "test" {
name_prefix = "tf-test-"
option_group_description = "Test option group for terraform"
engine_name = "mysql"
major_engine_version = "5.6"
}
`
const testAccAWSDBOptionGroup_generatedName = `
resource "aws_db_option_group" "test" {
option_group_description = "Test option group for terraform"
engine_name = "mysql"
major_engine_version = "5.6"
}
`
func testAccAWSDBOptionGroup_defaultDescription(n int) string {
return fmt.Sprintf(`
resource "aws_db_option_group" "test" {
name = "tf-test-%d"
engine_name = "mysql"
major_engine_version = "5.6"
}
`, n)
}

View File

@ -33,10 +33,19 @@ func resourceAwsDbParameterGroup() *schema.Resource {
},
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
Required: true,
ConflictsWith: []string{"name_prefix"},
ValidateFunc: validateDbParamGroupName,
},
"name_prefix": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validateDbParamGroupNamePrefix,
},
"family": &schema.Schema{
Type: schema.TypeString,
Required: true,
@ -81,8 +90,17 @@ func resourceAwsDbParameterGroupCreate(d *schema.ResourceData, meta interface{})
rdsconn := meta.(*AWSClient).rdsconn
tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{}))
var groupName string
if v, ok := d.GetOk("name"); ok {
groupName = v.(string)
} else if v, ok := d.GetOk("name_prefix"); ok {
groupName = resource.PrefixedUniqueId(v.(string))
} else {
groupName = resource.UniqueId()
}
createOpts := rds.CreateDBParameterGroupInput{
DBParameterGroupName: aws.String(d.Get("name").(string)),
DBParameterGroupName: aws.String(groupName),
DBParameterGroupFamily: aws.String(d.Get("family").(string)),
Description: aws.String(d.Get("description").(string)),
Tags: tags,

View File

@ -3,6 +3,7 @@ package aws
import (
"fmt"
"math/rand"
"regexp"
"testing"
"time"
@ -290,6 +291,44 @@ func TestAccAWSDBParameterGroup_basic(t *testing.T) {
})
}
func TestAccAWSDBParameterGroup_namePrefix(t *testing.T) {
var v rds.DBParameterGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBParameterGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDBParameterGroupConfig_namePrefix,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBParameterGroupExists("aws_db_parameter_group.test", &v),
resource.TestMatchResourceAttr(
"aws_db_parameter_group.test", "name", regexp.MustCompile("^tf-test-")),
),
},
},
})
}
func TestAccAWSDBParameterGroup_generatedName(t *testing.T) {
var v rds.DBParameterGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBParameterGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDBParameterGroupConfig_generatedName,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBParameterGroupExists("aws_db_parameter_group.test", &v),
),
},
},
})
}
func TestAccAWSDBParameterGroup_withApplyMethod(t *testing.T) {
var v rds.DBParameterGroup
@ -671,3 +710,16 @@ resource "aws_db_parameter_group" "large" {
parameter { name = "tx_isolation" value = "REPEATABLE-READ" }
}`, n)
}
const testAccDBParameterGroupConfig_namePrefix = `
resource "aws_db_parameter_group" "test" {
name_prefix = "tf-test-"
family = "mysql5.6"
}
`
const testAccDBParameterGroupConfig_generatedName = `
resource "aws_db_parameter_group" "test" {
family = "mysql5.6"
}
`

View File

@ -3,7 +3,6 @@ package aws
import (
"fmt"
"log"
"regexp"
"strings"
"time"
@ -32,9 +31,18 @@ func resourceAwsDbSubnetGroup() *schema.Resource {
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
Required: true,
ValidateFunc: validateSubnetGroupName,
ConflictsWith: []string{"name_prefix"},
ValidateFunc: validateDbSubnetGroupName,
},
"name_prefix": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validateDbSubnetGroupNamePrefix,
},
"description": &schema.Schema{
@ -65,8 +73,17 @@ func resourceAwsDbSubnetGroupCreate(d *schema.ResourceData, meta interface{}) er
subnetIds[i] = aws.String(subnetId.(string))
}
var groupName string
if v, ok := d.GetOk("name"); ok {
groupName = v.(string)
} else if v, ok := d.GetOk("name_prefix"); ok {
groupName = resource.PrefixedUniqueId(v.(string))
} else {
groupName = resource.UniqueId()
}
createOpts := rds.CreateDBSubnetGroupInput{
DBSubnetGroupName: aws.String(d.Get("name").(string)),
DBSubnetGroupName: aws.String(groupName),
DBSubnetGroupDescription: aws.String(d.Get("description").(string)),
SubnetIds: subnetIds,
Tags: tags,
@ -238,20 +255,3 @@ func buildRDSsubgrpARN(identifier, partition, accountid, region string) (string,
return arn, nil
}
func validateSubnetGroupName(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if !regexp.MustCompile(`^[ .0-9a-z-_]+$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"only lowercase alphanumeric characters, hyphens, underscores, periods, and spaces allowed in %q", k))
}
if len(value) > 255 {
errors = append(errors, fmt.Errorf(
"%q cannot be longer than 255 characters", k))
}
if regexp.MustCompile(`(?i)^default$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q is not allowed as %q", "Default", k))
}
return
}

View File

@ -2,6 +2,7 @@ package aws
import (
"fmt"
"regexp"
"testing"
"github.com/hashicorp/terraform/helper/acctest"
@ -43,6 +44,46 @@ func TestAccAWSDBSubnetGroup_basic(t *testing.T) {
})
}
func TestAccAWSDBSubnetGroup_namePrefix(t *testing.T) {
var v rds.DBSubnetGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDBSubnetGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDBSubnetGroupConfig_namePrefix,
Check: resource.ComposeTestCheckFunc(
testAccCheckDBSubnetGroupExists(
"aws_db_subnet_group.test", &v),
resource.TestMatchResourceAttr(
"aws_db_subnet_group.test", "name", regexp.MustCompile("^tf_test-")),
),
},
},
})
}
func TestAccAWSDBSubnetGroup_generatedName(t *testing.T) {
var v rds.DBSubnetGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDBSubnetGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDBSubnetGroupConfig_generatedName,
Check: resource.ComposeTestCheckFunc(
testAccCheckDBSubnetGroupExists(
"aws_db_subnet_group.test", &v),
),
},
},
})
}
// Regression test for https://github.com/hashicorp/terraform/issues/2603 and
// https://github.com/hashicorp/terraform/issues/2664
func TestAccAWSDBSubnetGroup_withUndocumentedCharacters(t *testing.T) {
@ -105,38 +146,6 @@ func TestAccAWSDBSubnetGroup_updateDescription(t *testing.T) {
})
}
func TestResourceAWSDBSubnetGroupNameValidation(t *testing.T) {
cases := []struct {
Value string
ErrCount int
}{
{
Value: "tEsting",
ErrCount: 1,
},
{
Value: "testing?",
ErrCount: 1,
},
{
Value: "default",
ErrCount: 1,
},
{
Value: randomString(300),
ErrCount: 1,
},
}
for _, tc := range cases {
_, errors := validateSubnetGroupName(tc.Value, "aws_db_subnet_group")
if len(errors) != tc.ErrCount {
t.Fatalf("Expected the DB Subnet Group name to trigger a validation error")
}
}
}
func testAccCheckDBSubnetGroupDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).rdsconn
@ -263,6 +272,49 @@ resource "aws_db_subnet_group" "foo" {
}`, rName)
}
const testAccDBSubnetGroupConfig_namePrefix = `
resource "aws_vpc" "test" {
cidr_block = "10.1.0.0/16"
}
resource "aws_subnet" "a" {
vpc_id = "${aws_vpc.test.id}"
cidr_block = "10.1.1.0/24"
availability_zone = "us-west-2a"
}
resource "aws_subnet" "b" {
vpc_id = "${aws_vpc.test.id}"
cidr_block = "10.1.2.0/24"
availability_zone = "us-west-2b"
}
resource "aws_db_subnet_group" "test" {
name_prefix = "tf_test-"
subnet_ids = ["${aws_subnet.a.id}", "${aws_subnet.b.id}"]
}`
const testAccDBSubnetGroupConfig_generatedName = `
resource "aws_vpc" "test" {
cidr_block = "10.1.0.0/16"
}
resource "aws_subnet" "a" {
vpc_id = "${aws_vpc.test.id}"
cidr_block = "10.1.1.0/24"
availability_zone = "us-west-2a"
}
resource "aws_subnet" "b" {
vpc_id = "${aws_vpc.test.id}"
cidr_block = "10.1.2.0/24"
availability_zone = "us-west-2b"
}
resource "aws_db_subnet_group" "test" {
subnet_ids = ["${aws_subnet.a.id}", "${aws_subnet.b.id}"]
}`
const testAccDBSubnetGroupConfig_withUnderscoresAndPeriodsAndSpaces = `
resource "aws_vpc" "main" {
cidr_block = "192.168.0.0/16"

View File

@ -36,6 +36,7 @@ func resourceAwsDirectoryServiceDirectory() *schema.Resource {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Sensitive: true,
},
"size": &schema.Schema{
Type: schema.TypeString,

View File

@ -169,7 +169,7 @@ resource "aws_dms_replication_instance" "dms_replication_instance" {
func dmsReplicationInstanceConfigUpdate(randId string) string {
return fmt.Sprintf(`
resource "aws_iam_role" "dms_iam_role" {
name = "dms-vpc-role"
name = "dms-vpc-role-%[1]s"
assume_role_policy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"dms.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"
}

View File

@ -102,7 +102,7 @@ func dmsReplicationSubnetGroupDestroy(s *terraform.State) error {
func dmsReplicationSubnetGroupConfig(randId string) string {
return fmt.Sprintf(`
resource "aws_iam_role" "dms_iam_role" {
name = "dms-vpc-role"
name = "dms-vpc-role-%[1]s"
assume_role_policy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"dms.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"
}

View File

@ -102,7 +102,7 @@ func dmsReplicationTaskDestroy(s *terraform.State) error {
func dmsReplicationTaskConfig(randId string) string {
return fmt.Sprintf(`
resource "aws_iam_role" "dms_iam_role" {
name = "dms-vpc-role"
name = "dms-vpc-role-%[1]s"
assume_role_policy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"dms.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"
}

View File

@ -118,6 +118,12 @@ func resourceAwsEcsService() *schema.Resource {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
if strings.ToLower(old) == strings.ToLower(new) {
return true
}
return false
},
},
},
},

View File

@ -45,6 +45,7 @@ func resourceAwsEcsTaskDefinition() *schema.Resource {
hash := sha1.Sum([]byte(v.(string)))
return hex.EncodeToString(hash[:])
},
ValidateFunc: validateAwsEcsTaskDefinitionContainerDefinitions,
},
"task_role_arn": {
@ -121,6 +122,15 @@ func validateAwsEcsTaskDefinitionNetworkMode(v interface{}, k string) (ws []stri
return
}
func validateAwsEcsTaskDefinitionContainerDefinitions(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
_, err := expandEcsContainerDefinitions(value)
if err != nil {
errors = append(errors, fmt.Errorf("ECS Task Definition container_definitions is invalid: %s", err))
}
return
}
func resourceAwsEcsTaskDefinitionCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ecsconn

View File

@ -203,6 +203,28 @@ func TestValidateAwsEcsTaskDefinitionNetworkMode(t *testing.T) {
}
}
func TestValidateAwsEcsTaskDefinitionContainerDefinitions(t *testing.T) {
validDefinitions := []string{
testValidateAwsEcsTaskDefinitionValidContainerDefinitions,
}
for _, v := range validDefinitions {
_, errors := validateAwsEcsTaskDefinitionContainerDefinitions(v, "container_definitions")
if len(errors) != 0 {
t.Fatalf("%q should be a valid AWS ECS Task Definition Container Definitions: %q", v, errors)
}
}
invalidDefinitions := []string{
testValidateAwsEcsTaskDefinitionInvalidCommandContainerDefinitions,
}
for _, v := range invalidDefinitions {
_, errors := validateAwsEcsTaskDefinitionContainerDefinitions(v, "container_definitions")
if len(errors) == 0 {
t.Fatalf("%q should be an invalid AWS ECS Task Definition Container Definitions", v)
}
}
}
func testAccCheckAWSEcsTaskDefinitionDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ecsconn
@ -666,3 +688,29 @@ TASK_DEFINITION
}
}
`
var testValidateAwsEcsTaskDefinitionValidContainerDefinitions = `
[
{
"name": "sleep",
"image": "busybox",
"cpu": 10,
"command": ["sleep","360"],
"memory": 10,
"essential": true
}
]
`
var testValidateAwsEcsTaskDefinitionInvalidCommandContainerDefinitions = `
[
{
"name": "sleep",
"image": "busybox",
"cpu": 10,
"command": "sleep 360",
"memory": 10,
"essential": true
}
]
`

View File

@ -82,6 +82,7 @@ func TestResourceAWSEFSFileSystem_hasEmptyFileSystems(t *testing.T) {
}
func TestAccAWSEFSFileSystem_basic(t *testing.T) {
rInt := acctest.RandInt()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
@ -104,7 +105,7 @@ func TestAccAWSEFSFileSystem_basic(t *testing.T) {
),
},
{
Config: testAccAWSEFSFileSystemConfigWithTags,
Config: testAccAWSEFSFileSystemConfigWithTags(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckEfsFileSystem(
"aws_efs_file_system.foo-with-tags",
@ -116,7 +117,7 @@ func TestAccAWSEFSFileSystem_basic(t *testing.T) {
testAccCheckEfsFileSystemTags(
"aws_efs_file_system.foo-with-tags",
map[string]string{
"Name": "foo-efs",
"Name": fmt.Sprintf("foo-efs-%d", rInt),
"Another": "tag",
},
),
@ -143,13 +144,14 @@ func TestAccAWSEFSFileSystem_basic(t *testing.T) {
}
func TestAccAWSEFSFileSystem_pagedTags(t *testing.T) {
rInt := acctest.RandInt()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckEfsFileSystemDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSEFSFileSystemConfigPagedTags,
Config: testAccAWSEFSFileSystemConfigPagedTags(rInt),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"aws_efs_file_system.foo",
@ -312,11 +314,12 @@ resource "aws_efs_file_system" "foo" {
}
`
const testAccAWSEFSFileSystemConfigPagedTags = `
func testAccAWSEFSFileSystemConfigPagedTags(rInt int) string {
return fmt.Sprintf(`
resource "aws_efs_file_system" "foo" {
creation_token = "radeksimko"
tags {
Name = "foo-efs"
Name = "foo-efs-%d"
Another = "tag"
Test = "yes"
User = "root"
@ -329,17 +332,20 @@ resource "aws_efs_file_system" "foo" {
Region = "us-west-2"
}
}
`
`, rInt)
}
const testAccAWSEFSFileSystemConfigWithTags = `
func testAccAWSEFSFileSystemConfigWithTags(rInt int) string {
return fmt.Sprintf(`
resource "aws_efs_file_system" "foo-with-tags" {
creation_token = "yada_yada"
tags {
Name = "foo-efs"
Name = "foo-efs-%d"
Another = "tag"
}
}
`
`, rInt)
}
const testAccAWSEFSFileSystemConfigWithPerformanceMode = `
resource "aws_efs_file_system" "foo-with-performance-mode" {

View File

@ -661,7 +661,7 @@ resource "aws_elastic_beanstalk_environment" "tfenvtest" {
func testAccBeanstalkWorkerEnvConfig(rInt int) string {
return fmt.Sprintf(`
resource "aws_iam_instance_profile" "tftest" {
name = "tftest_profile"
name = "tftest_profile-%d"
roles = ["${aws_iam_role.tftest.name}"]
}
@ -693,7 +693,7 @@ func testAccBeanstalkWorkerEnvConfig(rInt int) string {
name = "IamInstanceProfile"
value = "${aws_iam_instance_profile.tftest.name}"
}
}`, rInt, rInt)
}`, rInt, rInt, rInt)
}
func testAccBeanstalkEnvCnamePrefixConfig(randString string, rInt int) string {
@ -937,24 +937,24 @@ resource "aws_s3_bucket_object" "default" {
}
resource "aws_elastic_beanstalk_application" "default" {
name = "tf-test-name"
name = "tf-test-name-%d"
description = "tf-test-desc"
}
resource "aws_elastic_beanstalk_application_version" "default" {
application = "tf-test-name"
application = "tf-test-name-%d"
name = "tf-test-version-label"
bucket = "${aws_s3_bucket.default.id}"
key = "${aws_s3_bucket_object.default.id}"
}
resource "aws_elastic_beanstalk_environment" "default" {
name = "tf-test-name"
name = "tf-test-name-%d"
application = "${aws_elastic_beanstalk_application.default.name}"
version_label = "${aws_elastic_beanstalk_application_version.default.name}"
solution_stack_name = "64bit Amazon Linux running Python"
}
`, randInt)
`, randInt, randInt, randInt, randInt)
}
func testAccBeanstalkEnvApplicationVersionConfigUpdate(randInt int) string {
@ -970,22 +970,22 @@ resource "aws_s3_bucket_object" "default" {
}
resource "aws_elastic_beanstalk_application" "default" {
name = "tf-test-name"
name = "tf-test-name-%d"
description = "tf-test-desc"
}
resource "aws_elastic_beanstalk_application_version" "default" {
application = "tf-test-name"
application = "tf-test-name-%d"
name = "tf-test-version-label-v2"
bucket = "${aws_s3_bucket.default.id}"
key = "${aws_s3_bucket_object.default.id}"
}
resource "aws_elastic_beanstalk_environment" "default" {
name = "tf-test-name"
name = "tf-test-name-%d"
application = "${aws_elastic_beanstalk_application.default.name}"
version_label = "${aws_elastic_beanstalk_application_version.default.name}"
solution_stack_name = "64bit Amazon Linux running Python"
}
`, randInt)
`, randInt, randInt, randInt, randInt)
}

View File

@ -83,6 +83,7 @@ func resourceAwsElasticSearchDomain() *schema.Resource {
"volume_type": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},
},
},

View File

@ -85,6 +85,13 @@ func testAccESDomainPolicyConfig(randInt int, policy string) string {
resource "aws_elasticsearch_domain" "example" {
domain_name = "tf-test-%d"
elasticsearch_version = "2.3"
cluster_config {
instance_type = "t2.micro.elasticsearch"
}
ebs_options {
ebs_enabled = true
volume_size = 10
}
}
resource "aws_elasticsearch_domain_policy" "main" {

View File

@ -96,7 +96,7 @@ func TestAccAWSElasticSearchDomain_complex(t *testing.T) {
})
}
func TestAccAWSElasticSearch_tags(t *testing.T) {
func TestAccAWSElasticSearchDomain_tags(t *testing.T) {
var domain elasticsearch.ElasticsearchDomainStatus
var td elasticsearch.ListTagsOutput
ri := acctest.RandInt()
@ -198,6 +198,10 @@ func testAccESDomainConfig(randInt int) string {
return fmt.Sprintf(`
resource "aws_elasticsearch_domain" "example" {
domain_name = "tf-test-%d"
ebs_options {
ebs_enabled = true
volume_size = 10
}
}
`, randInt)
}
@ -206,6 +210,10 @@ func testAccESDomainConfig_TagUpdate(randInt int) string {
return fmt.Sprintf(`
resource "aws_elasticsearch_domain" "example" {
domain_name = "tf-test-%d"
ebs_options {
ebs_enabled = true
volume_size = 10
}
tags {
foo = "bar"
@ -220,6 +228,10 @@ func testAccESDomainConfig_complex(randInt int) string {
resource "aws_elasticsearch_domain" "example" {
domain_name = "tf-test-%d"
cluster_config {
instance_type = "r3.large.elasticsearch"
}
advanced_options {
"indices.fielddata.cache.size" = 80
}
@ -248,6 +260,10 @@ func testAccESDomainConfigV23(randInt int) string {
return fmt.Sprintf(`
resource "aws_elasticsearch_domain" "example" {
domain_name = "tf-test-%d"
ebs_options {
ebs_enabled = true
volume_size = 10
}
elasticsearch_version = "2.3"
}
`, randInt)

View File

@ -287,7 +287,7 @@ func resourceAwsElbCreate(d *schema.ResourceData, meta interface{}) error {
}
log.Printf("[DEBUG] ELB create configuration: %#v", elbOpts)
err = resource.Retry(1*time.Minute, func() *resource.RetryError {
err = resource.Retry(5*time.Minute, func() *resource.RetryError {
_, err := elbconn.CreateLoadBalancer(elbOpts)
if err != nil {
@ -488,7 +488,7 @@ func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
// Occasionally AWS will error with a 'duplicate listener', without any
// other listeners on the ELB. Retry here to eliminate that.
err := resource.Retry(1*time.Minute, func() *resource.RetryError {
err := resource.Retry(5*time.Minute, func() *resource.RetryError {
log.Printf("[DEBUG] ELB Create Listeners opts: %s", createListenersOpts)
if _, err := elbconn.CreateLoadBalancerListeners(createListenersOpts); err != nil {
if awsErr, ok := err.(awserr.Error); ok {
@ -746,7 +746,7 @@ func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
}
log.Printf("[DEBUG] ELB attach subnets opts: %s", attachOpts)
err := resource.Retry(1*time.Minute, func() *resource.RetryError {
err := resource.Retry(5*time.Minute, func() *resource.RetryError {
_, err := elbconn.AttachLoadBalancerToSubnets(attachOpts)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {

View File

@ -152,6 +152,7 @@ func resourceAwsKinesisFirehoseDeliveryStream() *schema.Resource {
"password": {
Type: schema.TypeString,
Required: true,
Sensitive: true,
},
"role_arn": {

View File

@ -297,14 +297,13 @@ func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) e
err := resource.Retry(10*time.Minute, func() *resource.RetryError {
_, err := conn.CreateFunction(params)
if err != nil {
log.Printf("[ERROR] Received %q, retrying CreateFunction", err)
if awserr, ok := err.(awserr.Error); ok {
if awserr.Code() == "InvalidParameterValueException" {
log.Printf("[DEBUG] InvalidParameterValueException creating Lambda Function: %s", awserr)
return resource.RetryableError(awserr)
}
}
log.Printf("[DEBUG] Error creating Lambda Function: %s", err)
if isAWSErr(err, "InvalidParameterValueException", "The role defined for the function cannot be assumed by Lambda") {
log.Printf("[DEBUG] Received %s, retrying CreateFunction", err)
return resource.RetryableError(err)
}
return resource.NonRetryableError(err)
}
return nil

View File

@ -0,0 +1,98 @@
package aws
import (
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/lightsail"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsLightsailStaticIp() *schema.Resource {
return &schema.Resource{
Create: resourceAwsLightsailStaticIpCreate,
Read: resourceAwsLightsailStaticIpRead,
Delete: resourceAwsLightsailStaticIpDelete,
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"ip_address": {
Type: schema.TypeString,
Computed: true,
},
"arn": {
Type: schema.TypeString,
Computed: true,
},
"support_code": {
Type: schema.TypeString,
Computed: true,
},
},
}
}
func resourceAwsLightsailStaticIpCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).lightsailconn
name := d.Get("name").(string)
log.Printf("[INFO] Allocating Lightsail Static IP: %q", name)
out, err := conn.AllocateStaticIp(&lightsail.AllocateStaticIpInput{
StaticIpName: aws.String(name),
})
if err != nil {
return err
}
log.Printf("[INFO] Lightsail Static IP allocated: %s", *out)
d.SetId(name)
return resourceAwsLightsailStaticIpRead(d, meta)
}
func resourceAwsLightsailStaticIpRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).lightsailconn
name := d.Get("name").(string)
log.Printf("[INFO] Reading Lightsail Static IP: %q", name)
out, err := conn.GetStaticIp(&lightsail.GetStaticIpInput{
StaticIpName: aws.String(name),
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "NotFoundException" {
log.Printf("[WARN] Lightsail Static IP (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}
}
return err
}
log.Printf("[INFO] Received Lightsail Static IP: %s", *out)
d.Set("arn", out.StaticIp.Arn)
d.Set("ip_address", out.StaticIp.IpAddress)
d.Set("support_code", out.StaticIp.SupportCode)
return nil
}
func resourceAwsLightsailStaticIpDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).lightsailconn
name := d.Get("name").(string)
log.Printf("[INFO] Deleting Lightsail Static IP: %q", name)
out, err := conn.ReleaseStaticIp(&lightsail.ReleaseStaticIpInput{
StaticIpName: aws.String(name),
})
if err != nil {
return err
}
log.Printf("[INFO] Deleted Lightsail Static IP: %s", *out)
return nil
}

View File

@ -0,0 +1,96 @@
package aws
import (
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/lightsail"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsLightsailStaticIpAttachment() *schema.Resource {
return &schema.Resource{
Create: resourceAwsLightsailStaticIpAttachmentCreate,
Read: resourceAwsLightsailStaticIpAttachmentRead,
Delete: resourceAwsLightsailStaticIpAttachmentDelete,
Schema: map[string]*schema.Schema{
"static_ip_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"instance_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}
func resourceAwsLightsailStaticIpAttachmentCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).lightsailconn
staticIpName := d.Get("static_ip_name").(string)
log.Printf("[INFO] Attaching Lightsail Static IP: %q", staticIpName)
out, err := conn.AttachStaticIp(&lightsail.AttachStaticIpInput{
StaticIpName: aws.String(staticIpName),
InstanceName: aws.String(d.Get("instance_name").(string)),
})
if err != nil {
return err
}
log.Printf("[INFO] Lightsail Static IP attached: %s", *out)
d.SetId(staticIpName)
return resourceAwsLightsailStaticIpAttachmentRead(d, meta)
}
func resourceAwsLightsailStaticIpAttachmentRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).lightsailconn
staticIpName := d.Get("static_ip_name").(string)
log.Printf("[INFO] Reading Lightsail Static IP: %q", staticIpName)
out, err := conn.GetStaticIp(&lightsail.GetStaticIpInput{
StaticIpName: aws.String(staticIpName),
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "NotFoundException" {
log.Printf("[WARN] Lightsail Static IP (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}
}
return err
}
if !*out.StaticIp.IsAttached {
log.Printf("[WARN] Lightsail Static IP (%s) is not attached, removing from state", d.Id())
d.SetId("")
return nil
}
log.Printf("[INFO] Received Lightsail Static IP: %s", *out)
d.Set("instance_name", out.StaticIp.AttachedTo)
return nil
}
func resourceAwsLightsailStaticIpAttachmentDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).lightsailconn
name := d.Get("static_ip_name").(string)
log.Printf("[INFO] Detaching Lightsail Static IP: %q", name)
out, err := conn.DetachStaticIp(&lightsail.DetachStaticIpInput{
StaticIpName: aws.String(name),
})
if err != nil {
return err
}
log.Printf("[INFO] Detached Lightsail Static IP: %s", *out)
return nil
}

View File

@ -0,0 +1,163 @@
package aws
import (
"errors"
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/lightsail"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSLightsailStaticIpAttachment_basic(t *testing.T) {
var staticIp lightsail.StaticIp
staticIpName := fmt.Sprintf("tf-test-lightsail-%s", acctest.RandString(5))
instanceName := fmt.Sprintf("tf-test-lightsail-%s", acctest.RandString(5))
keypairName := fmt.Sprintf("tf-test-lightsail-%s", acctest.RandString(5))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLightsailStaticIpAttachmentDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSLightsailStaticIpAttachmentConfig_basic(staticIpName, instanceName, keypairName),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSLightsailStaticIpAttachmentExists("aws_lightsail_static_ip_attachment.test", &staticIp),
),
},
},
})
}
func TestAccAWSLightsailStaticIpAttachment_disappears(t *testing.T) {
var staticIp lightsail.StaticIp
staticIpName := fmt.Sprintf("tf-test-lightsail-%s", acctest.RandString(5))
instanceName := fmt.Sprintf("tf-test-lightsail-%s", acctest.RandString(5))
keypairName := fmt.Sprintf("tf-test-lightsail-%s", acctest.RandString(5))
staticIpDestroy := func(*terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).lightsailconn
_, err := conn.DetachStaticIp(&lightsail.DetachStaticIpInput{
StaticIpName: aws.String(staticIpName),
})
if err != nil {
return fmt.Errorf("Error deleting Lightsail Static IP in disappear test")
}
return nil
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLightsailStaticIpAttachmentDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSLightsailStaticIpAttachmentConfig_basic(staticIpName, instanceName, keypairName),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSLightsailStaticIpAttachmentExists("aws_lightsail_static_ip_attachment.test", &staticIp),
staticIpDestroy,
),
ExpectNonEmptyPlan: true,
},
},
})
}
func testAccCheckAWSLightsailStaticIpAttachmentExists(n string, staticIp *lightsail.StaticIp) 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 errors.New("No Lightsail Static IP Attachment ID is set")
}
conn := testAccProvider.Meta().(*AWSClient).lightsailconn
resp, err := conn.GetStaticIp(&lightsail.GetStaticIpInput{
StaticIpName: aws.String(rs.Primary.ID),
})
if err != nil {
return err
}
if resp == nil || resp.StaticIp == nil {
return fmt.Errorf("Static IP (%s) not found", rs.Primary.ID)
}
if !*resp.StaticIp.IsAttached {
return fmt.Errorf("Static IP (%s) not attached", rs.Primary.ID)
}
*staticIp = *resp.StaticIp
return nil
}
}
func testAccCheckAWSLightsailStaticIpAttachmentDestroy(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_lightsail_static_ip_attachment" {
continue
}
conn := testAccProvider.Meta().(*AWSClient).lightsailconn
resp, err := conn.GetStaticIp(&lightsail.GetStaticIpInput{
StaticIpName: aws.String(rs.Primary.ID),
})
if err == nil {
if *resp.StaticIp.IsAttached {
return fmt.Errorf("Lightsail Static IP %q is still attached (to %q)", rs.Primary.ID, *resp.StaticIp.AttachedTo)
}
}
// Verify the error
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "NotFoundException" {
return nil
}
}
return err
}
return nil
}
func testAccAWSLightsailStaticIpAttachmentConfig_basic(staticIpName, instanceName, keypairName string) string {
return fmt.Sprintf(`
provider "aws" {
region = "us-east-1"
}
resource "aws_lightsail_static_ip_attachment" "test" {
static_ip_name = "${aws_lightsail_static_ip.test.name}"
instance_name = "${aws_lightsail_instance.test.name}"
}
resource "aws_lightsail_static_ip" "test" {
name = "%s"
}
resource "aws_lightsail_instance" "test" {
name = "%s"
availability_zone = "us-east-1b"
blueprint_id = "wordpress_4_6_1"
bundle_id = "micro_1_0"
key_pair_name = "${aws_lightsail_key_pair.test.name}"
}
resource "aws_lightsail_key_pair" "test" {
name = "%s"
}
`, staticIpName, instanceName, keypairName)
}

View File

@ -0,0 +1,138 @@
package aws
import (
"errors"
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/lightsail"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSLightsailStaticIp_basic(t *testing.T) {
var staticIp lightsail.StaticIp
staticIpName := fmt.Sprintf("tf-test-lightsail-%s", acctest.RandString(5))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLightsailStaticIpDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSLightsailStaticIpConfig_basic(staticIpName),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSLightsailStaticIpExists("aws_lightsail_static_ip.test", &staticIp),
),
},
},
})
}
func TestAccAWSLightsailStaticIp_disappears(t *testing.T) {
var staticIp lightsail.StaticIp
staticIpName := fmt.Sprintf("tf-test-lightsail-%s", acctest.RandString(5))
staticIpDestroy := func(*terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).lightsailconn
_, err := conn.ReleaseStaticIp(&lightsail.ReleaseStaticIpInput{
StaticIpName: aws.String(staticIpName),
})
if err != nil {
return fmt.Errorf("Error deleting Lightsail Static IP in disapear test")
}
return nil
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLightsailStaticIpDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSLightsailStaticIpConfig_basic(staticIpName),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSLightsailStaticIpExists("aws_lightsail_static_ip.test", &staticIp),
staticIpDestroy,
),
ExpectNonEmptyPlan: true,
},
},
})
}
func testAccCheckAWSLightsailStaticIpExists(n string, staticIp *lightsail.StaticIp) 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 errors.New("No Lightsail Static IP ID is set")
}
conn := testAccProvider.Meta().(*AWSClient).lightsailconn
resp, err := conn.GetStaticIp(&lightsail.GetStaticIpInput{
StaticIpName: aws.String(rs.Primary.ID),
})
if err != nil {
return err
}
if resp == nil || resp.StaticIp == nil {
return fmt.Errorf("Static IP (%s) not found", rs.Primary.ID)
}
*staticIp = *resp.StaticIp
return nil
}
}
func testAccCheckAWSLightsailStaticIpDestroy(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_lightsail_static_ip" {
continue
}
conn := testAccProvider.Meta().(*AWSClient).lightsailconn
resp, err := conn.GetStaticIp(&lightsail.GetStaticIpInput{
StaticIpName: aws.String(rs.Primary.ID),
})
if err == nil {
if resp.StaticIp != nil {
return fmt.Errorf("Lightsail Static IP %q still exists", rs.Primary.ID)
}
}
// Verify the error
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "NotFoundException" {
return nil
}
}
return err
}
return nil
}
func testAccAWSLightsailStaticIpConfig_basic(staticIpName string) string {
return fmt.Sprintf(`
provider "aws" {
region = "us-east-1"
}
resource "aws_lightsail_static_ip" "test" {
name = "%s"
}
`, staticIpName)
}

View File

@ -104,6 +104,7 @@ func resourceAwsOpsworksApplication() *schema.Resource {
"password": {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
},
"revision": {
@ -189,6 +190,7 @@ func resourceAwsOpsworksApplication() *schema.Resource {
"private_key": {
Type: schema.TypeString,
Required: true,
Sensitive: true,
StateFunc: func(v interface{}) string {
switch v.(type) {
case string:

View File

@ -781,7 +781,7 @@ func resourceAwsOpsworksInstanceCreate(d *schema.ResourceData, meta interface{})
d.Set("id", instanceId)
if v, ok := d.GetOk("state"); ok && v.(string) == "running" {
err := startOpsworksInstance(d, meta, false)
err := startOpsworksInstance(d, meta, true)
if err != nil {
return err
}
@ -860,7 +860,7 @@ func resourceAwsOpsworksInstanceUpdate(d *schema.ResourceData, meta interface{})
}
} else {
if status != "stopped" && status != "stopping" && status != "shutting_down" {
err := stopOpsworksInstance(d, meta, false)
err := stopOpsworksInstance(d, meta, true)
if err != nil {
return err
}

View File

@ -113,6 +113,7 @@ func resourceAwsOpsworksStack() *schema.Resource {
"password": {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
},
"revision": {

View File

@ -37,9 +37,18 @@ func resourceAwsRDSCluster() *schema.Resource {
"cluster_identifier": {
Type: schema.TypeString,
Required: true,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validateRdsId,
ConflictsWith: []string{"cluster_identifier_prefix"},
ValidateFunc: validateRdsIdentifier,
},
"cluster_identifier_prefix": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validateRdsIdentifierPrefix,
},
"cluster_members": {
@ -225,6 +234,19 @@ func resourceAwsRDSClusterCreate(d *schema.ResourceData, meta interface{}) error
conn := meta.(*AWSClient).rdsconn
tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{}))
var identifier string
if v, ok := d.GetOk("cluster_identifier"); ok {
identifier = v.(string)
} else {
if v, ok := d.GetOk("cluster_identifier_prefix"); ok {
identifier = resource.PrefixedUniqueId(v.(string))
} else {
identifier = resource.PrefixedUniqueId("tf-")
}
d.Set("cluster_identifier", identifier)
}
if _, ok := d.GetOk("snapshot_identifier"); ok {
opts := rds.RestoreDBClusterFromSnapshotInput{
DBClusterIdentifier: aws.String(d.Get("cluster_identifier").(string)),

View File

@ -3,6 +3,7 @@ package aws
import (
"fmt"
"log"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
@ -25,8 +26,17 @@ func resourceAwsRDSClusterInstance() *schema.Resource {
"identifier": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validateRdsId,
ConflictsWith: []string{"identifier_prefix"},
ValidateFunc: validateRdsIdentifier,
},
"identifier_prefix": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validateRdsIdentifierPrefix,
},
"db_subnet_group_name": {
@ -105,6 +115,27 @@ func resourceAwsRDSClusterInstance() *schema.Resource {
Computed: true,
},
"preferred_maintenance_window": {
Type: schema.TypeString,
Optional: true,
Computed: true,
StateFunc: func(v interface{}) string {
if v != nil {
value := v.(string)
return strings.ToLower(value)
}
return ""
},
ValidateFunc: validateOnceAWeekWindowFormat,
},
"preferred_backup_window": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validateOnceADayWindowFormat,
},
"monitoring_interval": {
Type: schema.TypeInt,
Optional: true,
@ -140,10 +171,14 @@ func resourceAwsRDSClusterInstanceCreate(d *schema.ResourceData, meta interface{
createOpts.DBParameterGroupName = aws.String(attr.(string))
}
if v := d.Get("identifier").(string); v != "" {
createOpts.DBInstanceIdentifier = aws.String(v)
if v, ok := d.GetOk("identifier"); ok {
createOpts.DBInstanceIdentifier = aws.String(v.(string))
} else {
createOpts.DBInstanceIdentifier = aws.String(resource.UniqueId())
if v, ok := d.GetOk("identifier_prefix"); ok {
createOpts.DBInstanceIdentifier = aws.String(resource.PrefixedUniqueId(v.(string)))
} else {
createOpts.DBInstanceIdentifier = aws.String(resource.PrefixedUniqueId("tf-"))
}
}
if attr, ok := d.GetOk("db_subnet_group_name"); ok {
@ -154,6 +189,14 @@ func resourceAwsRDSClusterInstanceCreate(d *schema.ResourceData, meta interface{
createOpts.MonitoringRoleArn = aws.String(attr.(string))
}
if attr, ok := d.GetOk("preferred_backup_window"); ok {
createOpts.PreferredBackupWindow = aws.String(attr.(string))
}
if attr, ok := d.GetOk("preferred_maintenance_window"); ok {
createOpts.PreferredMaintenanceWindow = aws.String(attr.(string))
}
if attr, ok := d.GetOk("monitoring_interval"); ok {
createOpts.MonitoringInterval = aws.Int64(int64(attr.(int)))
}
@ -239,6 +282,8 @@ func resourceAwsRDSClusterInstanceRead(d *schema.ResourceData, meta interface{})
d.Set("kms_key_id", db.KmsKeyId)
d.Set("auto_minor_version_upgrade", db.AutoMinorVersionUpgrade)
d.Set("promotion_tier", db.PromotionTier)
d.Set("preferred_backup_window", db.PreferredBackupWindow)
d.Set("preferred_maintenance_window", db.PreferredMaintenanceWindow)
if db.MonitoringInterval != nil {
d.Set("monitoring_interval", db.MonitoringInterval)
@ -290,6 +335,18 @@ func resourceAwsRDSClusterInstanceUpdate(d *schema.ResourceData, meta interface{
requestUpdate = true
}
if d.HasChange("preferred_backup_window") {
d.SetPartial("preferred_backup_window")
req.PreferredBackupWindow = aws.String(d.Get("preferred_backup_window").(string))
requestUpdate = true
}
if d.HasChange("preferred_maintenance_window") {
d.SetPartial("preferred_maintenance_window")
req.PreferredMaintenanceWindow = aws.String(d.Get("preferred_maintenance_window").(string))
requestUpdate = true
}
if d.HasChange("monitoring_interval") {
d.SetPartial("monitoring_interval")
req.MonitoringInterval = aws.Int64(int64(d.Get("monitoring_interval").(int)))

View File

@ -30,6 +30,8 @@ func TestAccAWSRDSClusterInstance_basic(t *testing.T) {
testAccCheckAWSClusterInstanceExists("aws_rds_cluster_instance.cluster_instances", &v),
testAccCheckAWSDBClusterInstanceAttributes(&v),
resource.TestCheckResourceAttr("aws_rds_cluster_instance.cluster_instances", "auto_minor_version_upgrade", "true"),
resource.TestCheckResourceAttrSet("aws_rds_cluster_instance.cluster_instances", "preferred_maintenance_window"),
resource.TestCheckResourceAttrSet("aws_rds_cluster_instance.cluster_instances", "preferred_backup_window"),
),
},
{
@ -44,6 +46,48 @@ func TestAccAWSRDSClusterInstance_basic(t *testing.T) {
})
}
func TestAccAWSRDSClusterInstance_namePrefix(t *testing.T) {
var v rds.DBInstance
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSClusterDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSClusterInstanceConfig_namePrefix(acctest.RandInt()),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSClusterInstanceExists("aws_rds_cluster_instance.test", &v),
testAccCheckAWSDBClusterInstanceAttributes(&v),
resource.TestMatchResourceAttr(
"aws_rds_cluster_instance.test", "identifier", regexp.MustCompile("^tf-cluster-instance-")),
),
},
},
})
}
func TestAccAWSRDSClusterInstance_generatedName(t *testing.T) {
var v rds.DBInstance
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSClusterDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSClusterInstanceConfig_generatedName(acctest.RandInt()),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSClusterInstanceExists("aws_rds_cluster_instance.test", &v),
testAccCheckAWSDBClusterInstanceAttributes(&v),
resource.TestMatchResourceAttr(
"aws_rds_cluster_instance.test", "identifier", regexp.MustCompile("^tf-")),
),
},
},
})
}
func TestAccAWSRDSClusterInstance_kmsKey(t *testing.T) {
var v rds.DBInstance
keyRegex := regexp.MustCompile("^arn:aws:kms:")
@ -254,6 +298,83 @@ resource "aws_db_parameter_group" "bar" {
`, n, n, n)
}
func testAccAWSClusterInstanceConfig_namePrefix(n int) string {
return fmt.Sprintf(`
resource "aws_rds_cluster_instance" "test" {
identifier_prefix = "tf-cluster-instance-"
cluster_identifier = "${aws_rds_cluster.test.id}"
instance_class = "db.r3.large"
}
resource "aws_rds_cluster" "test" {
cluster_identifier = "tf-aurora-cluster-%d"
master_username = "root"
master_password = "password"
db_subnet_group_name = "${aws_db_subnet_group.test.name}"
skip_final_snapshot = true
}
resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "a" {
vpc_id = "${aws_vpc.test.id}"
cidr_block = "10.0.0.0/24"
availability_zone = "us-west-2a"
}
resource "aws_subnet" "b" {
vpc_id = "${aws_vpc.test.id}"
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-2b"
}
resource "aws_db_subnet_group" "test" {
name = "tf-test-%d"
subnet_ids = ["${aws_subnet.a.id}", "${aws_subnet.b.id}"]
}
`, n, n)
}
func testAccAWSClusterInstanceConfig_generatedName(n int) string {
return fmt.Sprintf(`
resource "aws_rds_cluster_instance" "test" {
cluster_identifier = "${aws_rds_cluster.test.id}"
instance_class = "db.r3.large"
}
resource "aws_rds_cluster" "test" {
cluster_identifier = "tf-aurora-cluster-%d"
master_username = "root"
master_password = "password"
db_subnet_group_name = "${aws_db_subnet_group.test.name}"
skip_final_snapshot = true
}
resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "a" {
vpc_id = "${aws_vpc.test.id}"
cidr_block = "10.0.0.0/24"
availability_zone = "us-west-2a"
}
resource "aws_subnet" "b" {
vpc_id = "${aws_vpc.test.id}"
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-2b"
}
resource "aws_db_subnet_group" "test" {
name = "tf-test-%d"
subnet_ids = ["${aws_subnet.a.id}", "${aws_subnet.b.id}"]
}
`, n, n)
}
func testAccAWSClusterInstanceConfigKmsKey(n int) string {
return fmt.Sprintf(`

View File

@ -30,10 +30,19 @@ func resourceAwsRDSClusterParameterGroup() *schema.Resource {
},
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
Required: true,
ConflictsWith: []string{"name_prefix"},
ValidateFunc: validateDbParamGroupName,
},
"name_prefix": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validateDbParamGroupNamePrefix,
},
"family": &schema.Schema{
Type: schema.TypeString,
Required: true,
@ -86,8 +95,17 @@ func resourceAwsRDSClusterParameterGroupCreate(d *schema.ResourceData, meta inte
rdsconn := meta.(*AWSClient).rdsconn
tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{}))
var groupName string
if v, ok := d.GetOk("name"); ok {
groupName = v.(string)
} else if v, ok := d.GetOk("name_prefix"); ok {
groupName = resource.PrefixedUniqueId(v.(string))
} else {
groupName = resource.UniqueId()
}
createOpts := rds.CreateDBClusterParameterGroupInput{
DBClusterParameterGroupName: aws.String(d.Get("name").(string)),
DBClusterParameterGroupName: aws.String(groupName),
DBParameterGroupFamily: aws.String(d.Get("family").(string)),
Description: aws.String(d.Get("description").(string)),
Tags: tags,

View File

@ -3,6 +3,7 @@ package aws
import (
"errors"
"fmt"
"regexp"
"testing"
"time"
@ -90,6 +91,44 @@ func TestAccAWSDBClusterParameterGroup_basic(t *testing.T) {
})
}
func TestAccAWSDBClusterParameterGroup_namePrefix(t *testing.T) {
var v rds.DBClusterParameterGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBClusterParameterGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSDBClusterParameterGroupConfig_namePrefix,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBClusterParameterGroupExists("aws_rds_cluster_parameter_group.test", &v),
resource.TestMatchResourceAttr(
"aws_rds_cluster_parameter_group.test", "name", regexp.MustCompile("^tf-test-")),
),
},
},
})
}
func TestAccAWSDBClusterParameterGroup_generatedName(t *testing.T) {
var v rds.DBClusterParameterGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBClusterParameterGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSDBClusterParameterGroupConfig_generatedName,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBClusterParameterGroupExists("aws_rds_cluster_parameter_group.test", &v),
),
},
},
})
}
func TestAccAWSDBClusterParameterGroup_disappears(t *testing.T) {
var v rds.DBClusterParameterGroup
@ -365,3 +404,15 @@ func testAccAWSDBClusterParameterGroupOnlyConfig(name string) string {
family = "aurora5.6"
}`, name)
}
const testAccAWSDBClusterParameterGroupConfig_namePrefix = `
resource "aws_rds_cluster_parameter_group" "test" {
name_prefix = "tf-test-"
family = "aurora5.6"
}
`
const testAccAWSDBClusterParameterGroupConfig_generatedName = `
resource "aws_rds_cluster_parameter_group" "test" {
family = "aurora5.6"
}
`

View File

@ -40,6 +40,46 @@ func TestAccAWSRDSCluster_basic(t *testing.T) {
})
}
func TestAccAWSRDSCluster_namePrefix(t *testing.T) {
var v rds.DBCluster
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSClusterDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSClusterConfig_namePrefix(acctest.RandInt()),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSClusterExists("aws_rds_cluster.test", &v),
resource.TestMatchResourceAttr(
"aws_rds_cluster.test", "cluster_identifier", regexp.MustCompile("^tf-test-")),
),
},
},
})
}
func TestAccAWSRDSCluster_generatedName(t *testing.T) {
var v rds.DBCluster
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSClusterDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSClusterConfig_generatedName(acctest.RandInt()),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSClusterExists("aws_rds_cluster.test", &v),
resource.TestMatchResourceAttr(
"aws_rds_cluster.test", "cluster_identifier", regexp.MustCompile("^tf-")),
),
},
},
})
}
func TestAccAWSRDSCluster_takeFinalSnapshot(t *testing.T) {
var v rds.DBCluster
rInt := acctest.RandInt()
@ -322,6 +362,71 @@ resource "aws_rds_cluster" "default" {
}`, n)
}
func testAccAWSClusterConfig_namePrefix(n int) string {
return fmt.Sprintf(`
resource "aws_rds_cluster" "test" {
cluster_identifier_prefix = "tf-test-"
master_username = "root"
master_password = "password"
db_subnet_group_name = "${aws_db_subnet_group.test.name}"
skip_final_snapshot = true
}
resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "a" {
vpc_id = "${aws_vpc.test.id}"
cidr_block = "10.0.0.0/24"
availability_zone = "us-west-2a"
}
resource "aws_subnet" "b" {
vpc_id = "${aws_vpc.test.id}"
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-2b"
}
resource "aws_db_subnet_group" "test" {
name = "tf-test-%d"
subnet_ids = ["${aws_subnet.a.id}", "${aws_subnet.b.id}"]
}
`, n)
}
func testAccAWSClusterConfig_generatedName(n int) string {
return fmt.Sprintf(`
resource "aws_rds_cluster" "test" {
master_username = "root"
master_password = "password"
db_subnet_group_name = "${aws_db_subnet_group.test.name}"
skip_final_snapshot = true
}
resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "a" {
vpc_id = "${aws_vpc.test.id}"
cidr_block = "10.0.0.0/24"
availability_zone = "us-west-2a"
}
resource "aws_subnet" "b" {
vpc_id = "${aws_vpc.test.id}"
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-2b"
}
resource "aws_db_subnet_group" "test" {
name = "tf-test-%d"
subnet_ids = ["${aws_subnet.a.id}", "${aws_subnet.b.id}"]
}
`, n)
}
func testAccAWSClusterConfigWithFinalSnapshot(n int) string {
return fmt.Sprintf(`
resource "aws_rds_cluster" "default" {

View File

@ -5,6 +5,7 @@ import (
"testing"
"github.com/aws/aws-sdk-go/service/ses"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
@ -42,7 +43,7 @@ func testAccCheckSESConfigurationSetDestroy(s *terraform.State) error {
found := false
for _, element := range response.ConfigurationSets {
if *element.Name == "some-configuration-set" {
if *element.Name == fmt.Sprintf("some-configuration-set-%d", escRandomInteger) {
found = true
}
}
@ -77,7 +78,7 @@ func testAccCheckAwsSESConfigurationSetExists(n string) resource.TestCheckFunc {
found := false
for _, element := range response.ConfigurationSets {
if *element.Name == "some-configuration-set" {
if *element.Name == fmt.Sprintf("some-configuration-set-%d", escRandomInteger) {
found = true
}
}
@ -90,8 +91,9 @@ func testAccCheckAwsSESConfigurationSetExists(n string) resource.TestCheckFunc {
}
}
const testAccAWSSESConfigurationSetConfig = `
var escRandomInteger = acctest.RandInt()
var testAccAWSSESConfigurationSetConfig = fmt.Sprintf(`
resource "aws_ses_configuration_set" "test" {
name = "some-configuration-set"
name = "some-configuration-set-%d"
}
`
`, escRandomInteger)

View File

@ -0,0 +1,99 @@
package aws
import (
"fmt"
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ses"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsSesDomainIdentity() *schema.Resource {
return &schema.Resource{
Create: resourceAwsSesDomainIdentityCreate,
Read: resourceAwsSesDomainIdentityRead,
Delete: resourceAwsSesDomainIdentityDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"domain": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"verification_token": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}
func resourceAwsSesDomainIdentityCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).sesConn
domainName := d.Get("domain").(string)
createOpts := &ses.VerifyDomainIdentityInput{
Domain: aws.String(domainName),
}
_, err := conn.VerifyDomainIdentity(createOpts)
if err != nil {
return fmt.Errorf("Error requesting SES domain identity verification: %s", err)
}
d.SetId(domainName)
return resourceAwsSesDomainIdentityRead(d, meta)
}
func resourceAwsSesDomainIdentityRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).sesConn
domainName := d.Id()
d.Set("domain", domainName)
readOpts := &ses.GetIdentityVerificationAttributesInput{
Identities: []*string{
aws.String(domainName),
},
}
response, err := conn.GetIdentityVerificationAttributes(readOpts)
if err != nil {
log.Printf("[WARN] Error fetching identity verification attributes for %s: %s", d.Id(), err)
return err
}
verificationAttrs, ok := response.VerificationAttributes[domainName]
if !ok {
log.Printf("[WARN] Domain not listed in response when fetching verification attributes for %s", d.Id())
d.SetId("")
return nil
}
d.Set("verification_token", verificationAttrs.VerificationToken)
return nil
}
func resourceAwsSesDomainIdentityDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).sesConn
domainName := d.Get("domain").(string)
deleteOpts := &ses.DeleteIdentityInput{
Identity: aws.String(domainName),
}
_, err := conn.DeleteIdentity(deleteOpts)
if err != nil {
return fmt.Errorf("Error deleting SES domain identity: %s", err)
}
return nil
}

View File

@ -0,0 +1,100 @@
package aws
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ses"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAwsSESDomainIdentity_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
Providers: testAccProviders,
CheckDestroy: testAccCheckAwsSESDomainIdentityDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: fmt.Sprintf(
testAccAwsSESDomainIdentityConfig,
acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum),
),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsSESDomainIdentityExists("aws_ses_domain_identity.test"),
),
},
},
})
}
func testAccCheckAwsSESDomainIdentityDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).sesConn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_ses_domain_identity" {
continue
}
domain := rs.Primary.ID
params := &ses.GetIdentityVerificationAttributesInput{
Identities: []*string{
aws.String(domain),
},
}
response, err := conn.GetIdentityVerificationAttributes(params)
if err != nil {
return err
}
if response.VerificationAttributes[domain] != nil {
return fmt.Errorf("SES Domain Identity %s still exists. Failing!", domain)
}
}
return nil
}
func testAccCheckAwsSESDomainIdentityExists(n string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("SES Domain Identity not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("SES Domain Identity name not set")
}
domain := rs.Primary.ID
conn := testAccProvider.Meta().(*AWSClient).sesConn
params := &ses.GetIdentityVerificationAttributesInput{
Identities: []*string{
aws.String(domain),
},
}
response, err := conn.GetIdentityVerificationAttributes(params)
if err != nil {
return err
}
if response.VerificationAttributes[domain] == nil {
return fmt.Errorf("SES Domain Identity %s not found in AWS", domain)
}
return nil
}
}
const testAccAwsSESDomainIdentityConfig = `
resource "aws_ses_domain_identity" "test" {
domain = "%s.terraformtesting.com"
}
`

View File

@ -5,6 +5,7 @@ import (
"testing"
"github.com/aws/aws-sdk-go/service/ses"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
@ -46,7 +47,7 @@ func testAccCheckSESEventDestinationDestroy(s *terraform.State) error {
found := false
for _, element := range response.ConfigurationSets {
if *element.Name == "some-configuration-set" {
if *element.Name == fmt.Sprintf("some-configuration-set-%d", edRandomInteger) {
found = true
}
}
@ -81,7 +82,7 @@ func testAccCheckAwsSESEventDestinationExists(n string) resource.TestCheckFunc {
found := false
for _, element := range response.ConfigurationSets {
if *element.Name == "some-configuration-set" {
if *element.Name == fmt.Sprintf("some-configuration-set-%d", edRandomInteger) {
found = true
}
}
@ -94,7 +95,8 @@ func testAccCheckAwsSESEventDestinationExists(n string) resource.TestCheckFunc {
}
}
const testAccAWSSESEventDestinationConfig = `
var edRandomInteger = acctest.RandInt()
var testAccAWSSESEventDestinationConfig = fmt.Sprintf(`
resource "aws_s3_bucket" "bucket" {
bucket = "tf-test-bucket-format"
acl = "private"
@ -155,7 +157,7 @@ data "aws_iam_policy_document" "fh_felivery_document" {
}
resource "aws_ses_configuration_set" "test" {
name = "some-configuration-set"
name = "some-configuration-set-%d"
}
resource "aws_ses_event_destination" "kinesis" {
@ -182,4 +184,4 @@ resource "aws_ses_event_destination" "cloudwatch" {
value_source = "emailHeader"
}
}
`
`, edRandomInteger)

View File

@ -8,6 +8,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ses"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
@ -111,7 +112,7 @@ func testAccCheckAwsSESReceiptRuleExists(n string) resource.TestCheckFunc {
params := &ses.DescribeReceiptRuleInput{
RuleName: aws.String("basic"),
RuleSetName: aws.String("test-me"),
RuleSetName: aws.String(fmt.Sprintf("test-me-%d", srrsRandomInt)),
}
response, err := conn.DescribeReceiptRule(params)
@ -153,7 +154,7 @@ func testAccCheckAwsSESReceiptRuleOrder(n string) resource.TestCheckFunc {
conn := testAccProvider.Meta().(*AWSClient).sesConn
params := &ses.DescribeReceiptRuleSetInput{
RuleSetName: aws.String("test-me"),
RuleSetName: aws.String(fmt.Sprintf("test-me-%d", srrsRandomInt)),
}
response, err := conn.DescribeReceiptRuleSet(params)
@ -185,8 +186,8 @@ func testAccCheckAwsSESReceiptRuleActions(n string) resource.TestCheckFunc {
conn := testAccProvider.Meta().(*AWSClient).sesConn
params := &ses.DescribeReceiptRuleInput{
RuleName: aws.String("actions"),
RuleSetName: aws.String("test-me"),
RuleName: aws.String("actions4"),
RuleSetName: aws.String(fmt.Sprintf("test-me-%d", srrsRandomInt)),
}
response, err := conn.DescribeReceiptRule(params)
@ -227,9 +228,10 @@ func testAccCheckAwsSESReceiptRuleActions(n string) resource.TestCheckFunc {
}
}
const testAccAWSSESReceiptRuleBasicConfig = `
var srrsRandomInt = acctest.RandInt()
var testAccAWSSESReceiptRuleBasicConfig = fmt.Sprintf(`
resource "aws_ses_receipt_rule_set" "test" {
rule_set_name = "test-me"
rule_set_name = "test-me-%d"
}
resource "aws_ses_receipt_rule" "basic" {
@ -240,11 +242,11 @@ resource "aws_ses_receipt_rule" "basic" {
scan_enabled = true
tls_policy = "Require"
}
`
`, srrsRandomInt)
const testAccAWSSESReceiptRuleOrderConfig = `
var testAccAWSSESReceiptRuleOrderConfig = fmt.Sprintf(`
resource "aws_ses_receipt_rule_set" "test" {
rule_set_name = "test-me"
rule_set_name = "test-me-%d"
}
resource "aws_ses_receipt_rule" "second" {
@ -257,19 +259,19 @@ resource "aws_ses_receipt_rule" "first" {
name = "first"
rule_set_name = "${aws_ses_receipt_rule_set.test.rule_set_name}"
}
`
`, srrsRandomInt)
const testAccAWSSESReceiptRuleActionsConfig = `
var testAccAWSSESReceiptRuleActionsConfig = fmt.Sprintf(`
resource "aws_s3_bucket" "emails" {
bucket = "ses-terraform-emails"
}
resource "aws_ses_receipt_rule_set" "test" {
rule_set_name = "test-me"
rule_set_name = "test-me-%d"
}
resource "aws_ses_receipt_rule" "actions" {
name = "actions"
name = "actions4"
rule_set_name = "${aws_ses_receipt_rule_set.test.rule_set_name}"
add_header_action {
@ -289,4 +291,4 @@ resource "aws_ses_receipt_rule" "actions" {
position = 2
}
}
`
`, srrsRandomInt)

View File

@ -60,7 +60,6 @@ func resourceAwsVpc() *schema.Resource {
"assign_generated_ipv6_cidr_block": {
Type: schema.TypeBool,
ForceNew: true,
Optional: true,
Default: false,
},
@ -178,7 +177,7 @@ func resourceAwsVpcRead(d *schema.ResourceData, meta interface{}) error {
d.Set("tags", tagsToMap(vpc.Tags))
for _, a := range vpc.Ipv6CidrBlockAssociationSet {
if *a.Ipv6CidrBlockState.State == "associated" {
if *a.Ipv6CidrBlockState.State == "associated" { //we can only ever have 1 IPv6 block associated at once
d.Set("assign_generated_ipv6_cidr_block", true)
d.Set("ipv6_association_id", a.AssociationId)
d.Set("ipv6_cidr_block", a.Ipv6CidrBlock)
@ -344,6 +343,68 @@ func resourceAwsVpcUpdate(d *schema.ResourceData, meta interface{}) error {
d.SetPartial("enable_classiclink")
}
if d.HasChange("assign_generated_ipv6_cidr_block") && !d.IsNewResource() {
toAssign := d.Get("assign_generated_ipv6_cidr_block").(bool)
log.Printf("[INFO] Modifying assign_generated_ipv6_cidr_block to %#v", toAssign)
if toAssign {
modifyOpts := &ec2.AssociateVpcCidrBlockInput{
VpcId: &vpcid,
AmazonProvidedIpv6CidrBlock: aws.Bool(toAssign),
}
log.Printf("[INFO] Enabling assign_generated_ipv6_cidr_block vpc attribute for %s: %#v",
d.Id(), modifyOpts)
resp, err := conn.AssociateVpcCidrBlock(modifyOpts)
if err != nil {
return err
}
// Wait for the CIDR to become available
log.Printf(
"[DEBUG] Waiting for IPv6 CIDR (%s) to become associated",
d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"associating", "disassociated"},
Target: []string{"associated"},
Refresh: Ipv6CidrStateRefreshFunc(conn, d.Id(), *resp.Ipv6CidrBlockAssociation.AssociationId),
Timeout: 1 * time.Minute,
}
if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf(
"Error waiting for IPv6 CIDR (%s) to become associated: %s",
d.Id(), err)
}
} else {
modifyOpts := &ec2.DisassociateVpcCidrBlockInput{
AssociationId: aws.String(d.Get("ipv6_association_id").(string)),
}
log.Printf("[INFO] Disabling assign_generated_ipv6_cidr_block vpc attribute for %s: %#v",
d.Id(), modifyOpts)
if _, err := conn.DisassociateVpcCidrBlock(modifyOpts); err != nil {
return err
}
// Wait for the CIDR to become available
log.Printf(
"[DEBUG] Waiting for IPv6 CIDR (%s) to become disassociated",
d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"disassociating", "associated"},
Target: []string{"disassociated"},
Refresh: Ipv6CidrStateRefreshFunc(conn, d.Id(), d.Get("ipv6_association_id").(string)),
Timeout: 1 * time.Minute,
}
if _, err := stateConf.WaitForState(); err != nil {
return fmt.Errorf(
"Error waiting for IPv6 CIDR (%s) to become disassociated: %s",
d.Id(), err)
}
}
d.SetPartial("assign_generated_ipv6_cidr_block")
}
if err := setTags(conn, d); err != nil {
return err
} else {
@ -412,6 +473,41 @@ func VPCStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc {
}
}
func Ipv6CidrStateRefreshFunc(conn *ec2.EC2, id string, associationId string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
describeVpcOpts := &ec2.DescribeVpcsInput{
VpcIds: []*string{aws.String(id)},
}
resp, err := conn.DescribeVpcs(describeVpcOpts)
if err != nil {
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidVpcID.NotFound" {
resp = nil
} else {
log.Printf("Error on VPCStateRefresh: %s", err)
return nil, "", err
}
}
if resp == nil {
// Sometimes AWS just has consistency issues and doesn't see
// our instance yet. Return an empty state.
return nil, "", nil
}
if resp.Vpcs[0].Ipv6CidrBlockAssociationSet == nil {
return nil, "", nil
}
for _, association := range resp.Vpcs[0].Ipv6CidrBlockAssociationSet {
if *association.AssociationId == associationId {
return association, *association.Ipv6CidrBlockState.State, nil
}
}
return nil, "", nil
}
}
func resourceAwsVpcSetDefaultNetworkAcl(conn *ec2.EC2, d *schema.ResourceData) error {
filter1 := &ec2.Filter{
Name: aws.String("default"),

View File

@ -46,7 +46,7 @@ func TestAccAWSVpc_enableIpv6(t *testing.T) {
Steps: []resource.TestStep{
{
Config: testAccVpcConfigIpv6Enabled,
Check: resource.ComposeTestCheckFunc(
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckVpcExists("aws_vpc.foo", &vpc),
testAccCheckVpcCidr(&vpc, "10.1.0.0/16"),
resource.TestCheckResourceAttr(
@ -55,6 +55,34 @@ func TestAccAWSVpc_enableIpv6(t *testing.T) {
"aws_vpc.foo", "ipv6_association_id"),
resource.TestCheckResourceAttrSet(
"aws_vpc.foo", "ipv6_cidr_block"),
resource.TestCheckResourceAttr(
"aws_vpc.foo", "assign_generated_ipv6_cidr_block", "true"),
),
},
{
Config: testAccVpcConfigIpv6Disabled,
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckVpcExists("aws_vpc.foo", &vpc),
testAccCheckVpcCidr(&vpc, "10.1.0.0/16"),
resource.TestCheckResourceAttr(
"aws_vpc.foo", "cidr_block", "10.1.0.0/16"),
resource.TestCheckResourceAttr(
"aws_vpc.foo", "assign_generated_ipv6_cidr_block", "false"),
),
},
{
Config: testAccVpcConfigIpv6Enabled,
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckVpcExists("aws_vpc.foo", &vpc),
testAccCheckVpcCidr(&vpc, "10.1.0.0/16"),
resource.TestCheckResourceAttr(
"aws_vpc.foo", "cidr_block", "10.1.0.0/16"),
resource.TestCheckResourceAttrSet(
"aws_vpc.foo", "ipv6_association_id"),
resource.TestCheckResourceAttrSet(
"aws_vpc.foo", "ipv6_cidr_block"),
resource.TestCheckResourceAttr(
"aws_vpc.foo", "assign_generated_ipv6_cidr_block", "true"),
),
},
},
@ -283,6 +311,12 @@ resource "aws_vpc" "foo" {
}
`
const testAccVpcConfigIpv6Disabled = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
}
`
const testAccVpcConfigUpdate = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"

View File

@ -294,6 +294,11 @@ func resourceAwsVpnConnectionRead(d *schema.ResourceData, meta interface{}) erro
}
vpnConnection := resp.VpnConnections[0]
if vpnConnection == nil || *vpnConnection.State == "deleted" {
// Seems we have lost our VPN Connection
d.SetId("")
return nil
}
// Set attributes under the user's control.
d.Set("vpn_gateway_id", vpnConnection.VpnGatewayId)

View File

@ -3,6 +3,7 @@ package aws
import (
"fmt"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
@ -16,6 +17,8 @@ import (
func TestAccAWSVpnConnection_basic(t *testing.T) {
rInt := acctest.RandInt()
rBgpAsn := acctest.RandIntRange(64512, 65534)
var vpn ec2.VpnConnection
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "aws_vpn_connection.foo",
@ -30,6 +33,7 @@ func TestAccAWSVpnConnection_basic(t *testing.T) {
"aws_vpn_gateway.vpn_gateway",
"aws_customer_gateway.customer_gateway",
"aws_vpn_connection.foo",
&vpn,
),
),
},
@ -41,6 +45,7 @@ func TestAccAWSVpnConnection_basic(t *testing.T) {
"aws_vpn_gateway.vpn_gateway",
"aws_customer_gateway.customer_gateway",
"aws_vpn_connection.foo",
&vpn,
),
),
},
@ -51,6 +56,7 @@ func TestAccAWSVpnConnection_basic(t *testing.T) {
func TestAccAWSVpnConnection_withoutStaticRoutes(t *testing.T) {
rInt := acctest.RandInt()
rBgpAsn := acctest.RandIntRange(64512, 65534)
var vpn ec2.VpnConnection
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "aws_vpn_connection.foo",
@ -65,6 +71,7 @@ func TestAccAWSVpnConnection_withoutStaticRoutes(t *testing.T) {
"aws_vpn_gateway.vpn_gateway",
"aws_customer_gateway.customer_gateway",
"aws_vpn_connection.foo",
&vpn,
),
resource.TestCheckResourceAttr("aws_vpn_connection.foo", "static_routes_only", "false"),
),
@ -73,6 +80,74 @@ func TestAccAWSVpnConnection_withoutStaticRoutes(t *testing.T) {
})
}
func TestAccAWSVpnConnection_disappears(t *testing.T) {
var vpn ec2.VpnConnection
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccAwsVpnConnectionDestroy,
Steps: []resource.TestStep{
{
Config: testAccAwsVpnConnectionConfig,
Check: resource.ComposeTestCheckFunc(
testAccAwsVpnConnection(
"aws_vpc.vpc",
"aws_vpn_gateway.vpn_gateway",
"aws_customer_gateway.customer_gateway",
"aws_vpn_connection.foo",
&vpn,
),
testAccAWSVpnConnectionDisappears(&vpn),
),
ExpectNonEmptyPlan: true,
},
},
})
}
func testAccAWSVpnConnectionDisappears(connection *ec2.VpnConnection) resource.TestCheckFunc {
return func(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
_, err := conn.DeleteVpnConnection(&ec2.DeleteVpnConnectionInput{
VpnConnectionId: connection.VpnConnectionId,
})
if err != nil {
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidVpnConnectionID.NotFound" {
return nil
}
if err != nil {
return err
}
}
return resource.Retry(40*time.Minute, func() *resource.RetryError {
opts := &ec2.DescribeVpnConnectionsInput{
VpnConnectionIds: []*string{connection.VpnConnectionId},
}
resp, err := conn.DescribeVpnConnections(opts)
if err != nil {
cgw, ok := err.(awserr.Error)
if ok && cgw.Code() == "InvalidVpnConnectionID.NotFound" {
return nil
}
if ok && cgw.Code() == "IncorrectState" {
return resource.RetryableError(fmt.Errorf(
"Waiting for VPN Connection to be in the correct state: %v", connection.VpnConnectionId))
}
return resource.NonRetryableError(
fmt.Errorf("Error retrieving VPN Connection: %s", err))
}
if *resp.VpnConnections[0].State == "deleted" {
return nil
}
return resource.RetryableError(fmt.Errorf(
"Waiting for VPN Connection: %v", connection.VpnConnectionId))
})
}
}
func testAccAwsVpnConnectionDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
for _, rs := range s.RootModule().Resources {
@ -117,7 +192,8 @@ func testAccAwsVpnConnection(
vpcResource string,
vpnGatewayResource string,
customerGatewayResource string,
vpnConnectionResource string) resource.TestCheckFunc {
vpnConnectionResource string,
vpnConnection *ec2.VpnConnection) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[vpnConnectionResource]
if !ok {
@ -134,7 +210,7 @@ func testAccAwsVpnConnection(
ec2conn := testAccProvider.Meta().(*AWSClient).ec2conn
_, err := ec2conn.DescribeVpnConnections(&ec2.DescribeVpnConnectionsInput{
resp, err := ec2conn.DescribeVpnConnections(&ec2.DescribeVpnConnectionsInput{
VpnConnectionIds: []*string{aws.String(connection.Primary.ID)},
})
@ -142,6 +218,8 @@ func testAccAwsVpnConnection(
return err
}
*vpnConnection = *resp.VpnConnections[0]
return nil
}
}

View File

@ -12,7 +12,7 @@ import (
"github.com/hashicorp/terraform/helper/schema"
)
func validateRdsId(v interface{}, k string) (ws []string, errors []error) {
func validateRdsIdentifier(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if !regexp.MustCompile(`^[0-9a-z-]+$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
@ -33,6 +33,23 @@ func validateRdsId(v interface{}, k string) (ws []string, errors []error) {
return
}
func validateRdsIdentifierPrefix(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if !regexp.MustCompile(`^[0-9a-z-]+$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"only lowercase alphanumeric characters and hyphens allowed in %q", k))
}
if !regexp.MustCompile(`^[a-z]`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"first character of %q must be a letter", k))
}
if regexp.MustCompile(`--`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q cannot contain two consecutive hyphens", k))
}
return
}
func validateElastiCacheClusterId(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if (len(value) < 1) || (len(value) > 20) {
@ -103,7 +120,27 @@ func validateDbParamGroupName(v interface{}, k string) (ws []string, errors []er
"%q cannot be greater than 255 characters", k))
}
return
}
func validateDbParamGroupNamePrefix(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if !regexp.MustCompile(`^[0-9a-z-]+$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"only lowercase alphanumeric characters and hyphens allowed in %q", k))
}
if !regexp.MustCompile(`^[a-z]`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"first character of %q must be a letter", k))
}
if regexp.MustCompile(`--`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q cannot contain two consecutive hyphens", k))
}
if len(value) > 255 {
errors = append(errors, fmt.Errorf(
"%q cannot be greater than 226 characters", k))
}
return
}
func validateStreamViewType(v interface{}, k string) (ws []string, errors []error) {
@ -1041,3 +1078,79 @@ func validateApiGatewayUsagePlanQuotaSettings(v map[string]interface{}) (errors
return
}
func validateDbSubnetGroupName(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if !regexp.MustCompile(`^[ .0-9a-z-_]+$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"only lowercase alphanumeric characters, hyphens, underscores, periods, and spaces allowed in %q", k))
}
if len(value) > 255 {
errors = append(errors, fmt.Errorf(
"%q cannot be longer than 255 characters", k))
}
if regexp.MustCompile(`(?i)^default$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q is not allowed as %q", "Default", k))
}
return
}
func validateDbSubnetGroupNamePrefix(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if !regexp.MustCompile(`^[ .0-9a-z-_]+$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"only lowercase alphanumeric characters, hyphens, underscores, periods, and spaces allowed in %q", k))
}
if len(value) > 229 {
errors = append(errors, fmt.Errorf(
"%q cannot be longer than 229 characters", k))
}
return
}
func validateDbOptionGroupName(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if !regexp.MustCompile(`^[a-z]`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"first character of %q must be a letter", k))
}
if !regexp.MustCompile(`^[0-9A-Za-z-]+$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"only alphanumeric characters and hyphens allowed in %q", k))
}
if regexp.MustCompile(`--`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q cannot contain two consecutive hyphens", k))
}
if regexp.MustCompile(`-$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q cannot end with a hyphen", k))
}
if len(value) > 255 {
errors = append(errors, fmt.Errorf(
"%q cannot be greater than 255 characters", k))
}
return
}
func validateDbOptionGroupNamePrefix(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if !regexp.MustCompile(`^[a-z]`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"first character of %q must be a letter", k))
}
if !regexp.MustCompile(`^[0-9A-Za-z-]+$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"only alphanumeric characters and hyphens allowed in %q", k))
}
if regexp.MustCompile(`--`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q cannot contain two consecutive hyphens", k))
}
if len(value) > 229 {
errors = append(errors, fmt.Errorf(
"%q cannot be greater than 229 characters", k))
}
return
}

View File

@ -1785,3 +1785,131 @@ func TestValidateElbNamePrefix(t *testing.T) {
}
}
}
func TestValidateDbSubnetGroupName(t *testing.T) {
cases := []struct {
Value string
ErrCount int
}{
{
Value: "tEsting",
ErrCount: 1,
},
{
Value: "testing?",
ErrCount: 1,
},
{
Value: "default",
ErrCount: 1,
},
{
Value: randomString(300),
ErrCount: 1,
},
}
for _, tc := range cases {
_, errors := validateDbSubnetGroupName(tc.Value, "aws_db_subnet_group")
if len(errors) != tc.ErrCount {
t.Fatalf("Expected the DB Subnet Group name to trigger a validation error")
}
}
}
func TestValidateDbSubnetGroupNamePrefix(t *testing.T) {
cases := []struct {
Value string
ErrCount int
}{
{
Value: "tEsting",
ErrCount: 1,
},
{
Value: "testing?",
ErrCount: 1,
},
{
Value: randomString(230),
ErrCount: 1,
},
}
for _, tc := range cases {
_, errors := validateDbSubnetGroupNamePrefix(tc.Value, "aws_db_subnet_group")
if len(errors) != tc.ErrCount {
t.Fatalf("Expected the DB Subnet Group name prefix to trigger a validation error")
}
}
}
func TestValidateDbOptionGroupName(t *testing.T) {
cases := []struct {
Value string
ErrCount int
}{
{
Value: "testing123!",
ErrCount: 1,
},
{
Value: "1testing123",
ErrCount: 1,
},
{
Value: "testing--123",
ErrCount: 1,
},
{
Value: "testing123-",
ErrCount: 1,
},
{
Value: randomString(256),
ErrCount: 1,
},
}
for _, tc := range cases {
_, errors := validateDbOptionGroupName(tc.Value, "aws_db_option_group_name")
if len(errors) != tc.ErrCount {
t.Fatalf("Expected the DB Option Group Name to trigger a validation error")
}
}
}
func TestValidateDbOptionGroupNamePrefix(t *testing.T) {
cases := []struct {
Value string
ErrCount int
}{
{
Value: "testing123!",
ErrCount: 1,
},
{
Value: "1testing123",
ErrCount: 1,
},
{
Value: "testing--123",
ErrCount: 1,
},
{
Value: randomString(230),
ErrCount: 1,
},
}
for _, tc := range cases {
_, errors := validateDbOptionGroupNamePrefix(tc.Value, "aws_db_option_group_name")
if len(errors) != tc.ErrCount {
t.Fatalf("Expected the DB Option Group name prefix to trigger a validation error")
}
}
}

View File

@ -66,6 +66,7 @@ func resourceArmNetworkSecurityGroup() *schema.Resource {
Type: schema.TypeString,
Required: true,
ValidateFunc: validateNetworkSecurityRuleProtocol,
StateFunc: ignoreCaseStateFunc,
},
"source_port_range": {

View File

@ -204,7 +204,7 @@ resource "azurerm_network_security_group" "test" {
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
protocol = "TCP"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"

View File

@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"testing"
@ -117,8 +118,13 @@ func buildDataSourceTestProgram() (string, error) {
return "", fmt.Errorf("failed to build test stub program: %s", err)
}
gopath := os.Getenv("GOPATH")
if gopath == "" {
gopath = filepath.Join(os.Getenv("HOME") + "/go")
}
programPath := path.Join(
os.Getenv("GOPATH"), "bin", "tf-acc-external-data-source",
filepath.SplitList(gopath)[0], "bin", "tf-acc-external-data-source",
)
return programPath, nil
}

View File

@ -10,9 +10,9 @@ import (
func resourceGithubIssueLabel() *schema.Resource {
return &schema.Resource{
Create: resourceGithubIssueLabelCreate,
Create: resourceGithubIssueLabelCreateOrUpdate,
Read: resourceGithubIssueLabelRead,
Update: resourceGithubIssueLabelUpdate,
Update: resourceGithubIssueLabelCreateOrUpdate,
Delete: resourceGithubIssueLabelDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
@ -40,22 +40,55 @@ func resourceGithubIssueLabel() *schema.Resource {
}
}
func resourceGithubIssueLabelCreate(d *schema.ResourceData, meta interface{}) error {
// resourceGithubIssueLabelCreateOrUpdate idempotently creates or updates an
// issue label. Issue labels are keyed off of their "name", so pre-existing
// issue labels result in a 422 HTTP error if they exist outside of Terraform.
// Normally this would not be an issue, except new repositories are created with
// a "default" set of labels, and those labels easily conflict with custom ones.
//
// This function will first check if the label exists, and then issue an update,
// otherwise it will create. This is also advantageous in that we get to use the
// same function for two schema funcs.
func resourceGithubIssueLabelCreateOrUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Organization).client
o := meta.(*Organization).name
r := d.Get("repository").(string)
n := d.Get("name").(string)
c := d.Get("color").(string)
label := github.Label{
label := &github.Label{
Name: &n,
Color: &c,
}
log.Printf("[DEBUG] Creating label: %#v", label)
_, resp, err := client.Issues.CreateLabel(context.TODO(), meta.(*Organization).name, r, &label)
log.Printf("[DEBUG] Querying label existence %s/%s (%s)", o, r, n)
existing, _, _ := client.Issues.GetLabel(context.TODO(), o, r, n)
if existing != nil {
log.Printf("[DEBUG] Updating label: %s/%s (%s: %s)", o, r, n, c)
// Pull out the original name. If we already have a resource, this is the
// parsed ID. If not, it's the value given to the resource.
var oname string
if d.Id() == "" {
oname = n
} else {
_, oname = parseTwoPartID(d.Id())
}
_, _, err := client.Issues.EditLabel(context.TODO(), o, r, oname, label)
if err != nil {
return err
}
} else {
log.Printf("[DEBUG] Creating label: %s/%s (%s: %s)", o, r, n, c)
_, resp, err := client.Issues.CreateLabel(context.TODO(), o, r, label)
log.Printf("[DEBUG] Response from creating label: %s", *resp)
if err != nil {
return err
}
}
d.SetId(buildTwoPartID(&r, &n))
@ -66,6 +99,7 @@ func resourceGithubIssueLabelRead(d *schema.ResourceData, meta interface{}) erro
client := meta.(*Organization).client
r, n := parseTwoPartID(d.Id())
log.Printf("[DEBUG] Reading label: %s/%s", r, n)
githubLabel, _, err := client.Issues.GetLabel(context.TODO(), meta.(*Organization).name, r, n)
if err != nil {
d.SetId("")
@ -80,31 +114,12 @@ func resourceGithubIssueLabelRead(d *schema.ResourceData, meta interface{}) erro
return nil
}
func resourceGithubIssueLabelUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Organization).client
r := d.Get("repository").(string)
n := d.Get("name").(string)
c := d.Get("color").(string)
_, originalName := parseTwoPartID(d.Id())
_, _, err := client.Issues.EditLabel(context.TODO(), meta.(*Organization).name, r, originalName, &github.Label{
Name: &n,
Color: &c,
})
if err != nil {
return err
}
d.SetId(buildTwoPartID(&r, &n))
return resourceGithubIssueLabelRead(d, meta)
}
func resourceGithubIssueLabelDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Organization).client
r := d.Get("repository").(string)
n := d.Get("name").(string)
log.Printf("[DEBUG] Deleting label: %s/%s", r, n)
_, err := client.Issues.DeleteLabel(context.TODO(), meta.(*Organization).name, r, n)
return err
}

View File

@ -32,6 +32,13 @@ func TestAccGithubIssueLabel_basic(t *testing.T) {
testAccCheckGithubIssueLabelAttributes(&label, "bar", "FFFFFF"),
),
},
{
Config: testAccGitHubIssueLabelExistsConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckGithubIssueLabelExists("github_issue_label.test", &label),
testAccCheckGithubIssueLabelAttributes(&label, "enhancement", "FF00FF"),
),
},
},
})
}
@ -134,3 +141,16 @@ resource "github_issue_label" "test" {
color = "FFFFFF"
}
`, testRepo)
var testAccGitHubIssueLabelExistsConfig string = fmt.Sprintf(`
// Create a repository which has the default labels
resource "github_repository" "test" {
name = "tf-acc-repo-label-abc1234"
}
resource "github_issue_label" "test" {
repository = "${github_repository.test.name}"
name = "enhancement" // Important! This is a pre-created label
color = "FF00FF"
}
`)

View File

@ -42,6 +42,7 @@ func resourceContainerCluster() *schema.Resource {
"client_key": &schema.Schema{
Type: schema.TypeString,
Computed: true,
Sensitive: true,
},
"cluster_ca_certificate": &schema.Schema{
Type: schema.TypeString,
@ -51,6 +52,7 @@ func resourceContainerCluster() *schema.Resource {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Sensitive: true,
},
"username": &schema.Schema{
Type: schema.TypeString,

View File

@ -83,6 +83,7 @@ func Provider() terraform.ResourceProvider {
ResourcesMap: map[string]*schema.Resource{
"kubernetes_config_map": resourceKubernetesConfigMap(),
"kubernetes_namespace": resourceKubernetesNamespace(),
"kubernetes_secret": resourceKubernetesSecret(),
},
ConfigureFunc: providerConfigure,
}

View File

@ -0,0 +1,159 @@
package kubernetes
import (
"log"
"fmt"
"github.com/hashicorp/terraform/helper/schema"
pkgApi "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors"
api "k8s.io/kubernetes/pkg/api/v1"
kubernetes "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
)
func resourceKubernetesSecret() *schema.Resource {
return &schema.Resource{
Create: resourceKubernetesSecretCreate,
Read: resourceKubernetesSecretRead,
Exists: resourceKubernetesSecretExists,
Update: resourceKubernetesSecretUpdate,
Delete: resourceKubernetesSecretDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"metadata": namespacedMetadataSchema("secret", true),
"data": {
Type: schema.TypeMap,
Description: "A map of the secret data.",
Optional: true,
Sensitive: true,
},
"type": {
Type: schema.TypeString,
Description: "Type of secret",
Default: "Opaque",
Optional: true,
ForceNew: true,
},
},
}
}
func resourceKubernetesSecretCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*kubernetes.Clientset)
metadata := expandMetadata(d.Get("metadata").([]interface{}))
secret := api.Secret{
ObjectMeta: metadata,
StringData: expandStringMap(d.Get("data").(map[string]interface{})),
}
if v, ok := d.GetOk("type"); ok {
secret.Type = api.SecretType(v.(string))
}
log.Printf("[INFO] Creating new secret: %#v", secret)
out, err := conn.CoreV1().Secrets(metadata.Namespace).Create(&secret)
if err != nil {
return err
}
log.Printf("[INFO] Submitting new secret: %#v", out)
d.SetId(buildId(out.ObjectMeta))
return resourceKubernetesSecretRead(d, meta)
}
func resourceKubernetesSecretRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*kubernetes.Clientset)
namespace, name := idParts(d.Id())
log.Printf("[INFO] Reading secret %s", name)
secret, err := conn.CoreV1().Secrets(namespace).Get(name)
if err != nil {
return err
}
log.Printf("[INFO] Received secret: %#v", secret)
err = d.Set("metadata", flattenMetadata(secret.ObjectMeta))
if err != nil {
return err
}
d.Set("data", byteMapToStringMap(secret.Data))
d.Set("type", secret.Type)
return nil
}
func resourceKubernetesSecretUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*kubernetes.Clientset)
namespace, name := idParts(d.Id())
ops := patchMetadata("metadata.0.", "/metadata/", d)
if d.HasChange("data") {
oldV, newV := d.GetChange("data")
oldV = base64EncodeStringMap(oldV.(map[string]interface{}))
newV = base64EncodeStringMap(newV.(map[string]interface{}))
diffOps := diffStringMap("/data/", oldV.(map[string]interface{}), newV.(map[string]interface{}))
ops = append(ops, diffOps...)
}
data, err := ops.MarshalJSON()
if err != nil {
return fmt.Errorf("Failed to marshal update operations: %s", err)
}
log.Printf("[INFO] Updating secret %q: %v", name, data)
out, err := conn.CoreV1().Secrets(namespace).Patch(name, pkgApi.JSONPatchType, data)
if err != nil {
return fmt.Errorf("Failed to update secret: %s", err)
}
log.Printf("[INFO] Submitting updated secret: %#v", out)
d.SetId(buildId(out.ObjectMeta))
return resourceKubernetesSecretRead(d, meta)
}
func resourceKubernetesSecretDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*kubernetes.Clientset)
namespace, name := idParts(d.Id())
log.Printf("[INFO] Deleting secret: %q", name)
err := conn.CoreV1().Secrets(namespace).Delete(name, &api.DeleteOptions{})
if err != nil {
return err
}
log.Printf("[INFO] Secret %s deleted", name)
d.SetId("")
return nil
}
func resourceKubernetesSecretExists(d *schema.ResourceData, meta interface{}) (bool, error) {
conn := meta.(*kubernetes.Clientset)
namespace, name := idParts(d.Id())
log.Printf("[INFO] Checking secret %s", name)
_, err := conn.CoreV1().Secrets(namespace).Get(name)
if err != nil {
if statusErr, ok := err.(*errors.StatusError); ok && statusErr.ErrStatus.Code == 404 {
return false, nil
}
log.Printf("[DEBUG] Received error: %#v", err)
}
return true, err
}

View File

@ -0,0 +1,320 @@
package kubernetes
import (
"fmt"
"reflect"
"regexp"
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
api "k8s.io/kubernetes/pkg/api/v1"
kubernetes "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
)
func TestAccKubernetesSecret_basic(t *testing.T) {
var conf api.Secret
name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "kubernetes_secret.test",
Providers: testAccProviders,
CheckDestroy: testAccCheckKubernetesSecretDestroy,
Steps: []resource.TestStep{
{
Config: testAccKubernetesSecretConfig_basic(name),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesSecretExists("kubernetes_secret.test", &conf),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.annotations.%", "2"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.annotations.TestAnnotationOne", "one"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.annotations.TestAnnotationTwo", "two"),
testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{"TestAnnotationOne": "one", "TestAnnotationTwo": "two"}),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.labels.%", "3"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.labels.TestLabelOne", "one"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.labels.TestLabelTwo", "two"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.labels.TestLabelThree", "three"),
testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{"TestLabelOne": "one", "TestLabelTwo": "two", "TestLabelThree": "three"}),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.name", name),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.generation"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.resource_version"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.self_link"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.uid"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "data.%", "2"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "data.one", "first"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "data.two", "second"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "type", "Opaque"),
testAccCheckSecretData(&conf, map[string]string{"one": "first", "two": "second"}),
),
},
{
Config: testAccKubernetesSecretConfig_modified(name),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesSecretExists("kubernetes_secret.test", &conf),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.annotations.%", "2"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.annotations.TestAnnotationOne", "one"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.annotations.Different", "1234"),
testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{"TestAnnotationOne": "one", "Different": "1234"}),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.labels.%", "2"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.labels.TestLabelOne", "one"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.labels.TestLabelThree", "three"),
testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{"TestLabelOne": "one", "TestLabelThree": "three"}),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.name", name),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.generation"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.resource_version"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.self_link"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.uid"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "data.%", "3"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "data.one", "first"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "data.two", "second"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "data.nine", "ninth"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "type", "Opaque"),
testAccCheckSecretData(&conf, map[string]string{"one": "first", "two": "second", "nine": "ninth"}),
),
},
{
Config: testAccKubernetesSecretConfig_noData(name),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesSecretExists("kubernetes_secret.test", &conf),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.annotations.%", "0"),
testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{}),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.labels.%", "0"),
testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{}),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.name", name),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.generation"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.resource_version"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.self_link"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.uid"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "data.%", "0"),
testAccCheckSecretData(&conf, map[string]string{}),
),
},
{
Config: testAccKubernetesSecretConfig_typeSpecified(name),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesSecretExists("kubernetes_secret.test", &conf),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.annotations.%", "0"),
testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{}),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.labels.%", "0"),
testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{}),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.name", name),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.generation"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.resource_version"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.self_link"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.uid"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "data.%", "2"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "data.username", "admin"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "data.password", "password"),
resource.TestCheckResourceAttr("kubernetes_secret.test", "type", "kubernetes.io/basic-auth"),
testAccCheckSecretData(&conf, map[string]string{"username": "admin", "password": "password"}),
),
},
},
})
}
func TestAccKubernetesSecret_importBasic(t *testing.T) {
resourceName := "kubernetes_secret.test"
name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckKubernetesSecretDestroy,
Steps: []resource.TestStep{
{
Config: testAccKubernetesSecretConfig_basic(name),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
func TestAccKubernetesSecret_generatedName(t *testing.T) {
var conf api.Secret
prefix := "tf-acc-test-gen-"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "kubernetes_secret.test",
Providers: testAccProviders,
CheckDestroy: testAccCheckKubernetesSecretDestroy,
Steps: []resource.TestStep{
{
Config: testAccKubernetesSecretConfig_generatedName(prefix),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesSecretExists("kubernetes_secret.test", &conf),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.annotations.%", "0"),
testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{}),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.labels.%", "0"),
testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{}),
resource.TestCheckResourceAttr("kubernetes_secret.test", "metadata.0.generate_name", prefix),
resource.TestMatchResourceAttr("kubernetes_secret.test", "metadata.0.name", regexp.MustCompile("^"+prefix)),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.generation"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.resource_version"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.self_link"),
resource.TestCheckResourceAttrSet("kubernetes_secret.test", "metadata.0.uid"),
),
},
},
})
}
func TestAccKubernetesSecret_importGeneratedName(t *testing.T) {
resourceName := "kubernetes_secret.test"
prefix := "tf-acc-test-gen-import-"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckKubernetesSecretDestroy,
Steps: []resource.TestStep{
{
Config: testAccKubernetesSecretConfig_generatedName(prefix),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
func testAccCheckSecretData(m *api.Secret, expected map[string]string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if len(expected) == 0 && len(m.Data) == 0 {
return nil
}
if !reflect.DeepEqual(byteMapToStringMap(m.Data), expected) {
return fmt.Errorf("%s data don't match.\nExpected: %q\nGiven: %q",
m.Name, expected, m.Data)
}
return nil
}
}
func testAccCheckKubernetesSecretDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*kubernetes.Clientset)
for _, rs := range s.RootModule().Resources {
if rs.Type != "kubernetes_secret" {
continue
}
namespace, name := idParts(rs.Primary.ID)
resp, err := conn.CoreV1().Secrets(namespace).Get(name)
if err == nil {
if resp.Name == rs.Primary.ID {
return fmt.Errorf("Secret still exists: %s", rs.Primary.ID)
}
}
}
return nil
}
func testAccCheckKubernetesSecretExists(n string, obj *api.Secret) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
conn := testAccProvider.Meta().(*kubernetes.Clientset)
namespace, name := idParts(rs.Primary.ID)
out, err := conn.CoreV1().Secrets(namespace).Get(name)
if err != nil {
return err
}
*obj = *out
return nil
}
}
func testAccKubernetesSecretConfig_basic(name string) string {
return fmt.Sprintf(`
resource "kubernetes_secret" "test" {
metadata {
annotations {
TestAnnotationOne = "one"
TestAnnotationTwo = "two"
}
labels {
TestLabelOne = "one"
TestLabelTwo = "two"
TestLabelThree = "three"
}
name = "%s"
}
data {
one = "first"
two = "second"
}
}`, name)
}
func testAccKubernetesSecretConfig_modified(name string) string {
return fmt.Sprintf(`
resource "kubernetes_secret" "test" {
metadata {
annotations {
TestAnnotationOne = "one"
Different = "1234"
}
labels {
TestLabelOne = "one"
TestLabelThree = "three"
}
name = "%s"
}
data {
one = "first"
two = "second"
nine = "ninth"
}
}`, name)
}
func testAccKubernetesSecretConfig_noData(name string) string {
return fmt.Sprintf(`
resource "kubernetes_secret" "test" {
metadata {
name = "%s"
}
}`, name)
}
func testAccKubernetesSecretConfig_typeSpecified(name string) string {
return fmt.Sprintf(`
resource "kubernetes_secret" "test" {
metadata {
name = "%s"
}
data {
username = "admin"
password = "password"
}
type = "kubernetes.io/basic-auth"
}`, name)
}
func testAccKubernetesSecretConfig_generatedName(prefix string) string {
return fmt.Sprintf(`
resource "kubernetes_secret" "test" {
metadata {
generate_name = "%s"
}
data {
one = "first"
two = "second"
}
}`, prefix)
}

View File

@ -5,6 +5,7 @@ import (
"net/url"
"strings"
"encoding/base64"
"github.com/hashicorp/terraform/helper/schema"
api "k8s.io/kubernetes/pkg/api/v1"
)
@ -99,3 +100,20 @@ func isInternalAnnotationKey(annotationKey string) bool {
return false
}
func byteMapToStringMap(m map[string][]byte) map[string]string {
result := make(map[string]string)
for k, v := range m {
result[k] = string(v)
}
return result
}
func base64EncodeStringMap(m map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for k, v := range m {
value := v.(string)
result[k] = (base64.StdEncoding.EncodeToString([]byte(value)))
}
return result
}

View File

@ -1,41 +1,43 @@
package triton
import (
"fmt"
"log"
"os"
"crypto/md5"
"encoding/base64"
"errors"
"sort"
"time"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"github.com/joyent/gocommon/client"
"github.com/joyent/gosdc/cloudapi"
"github.com/joyent/gosign/auth"
"github.com/joyent/triton-go"
"github.com/joyent/triton-go/authentication"
)
// Provider returns a terraform.ResourceProvider.
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"account": &schema.Schema{
"account": {
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("SDC_ACCOUNT", ""),
},
"url": &schema.Schema{
"url": {
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("SDC_URL", "https://us-west-1.api.joyentcloud.com"),
},
"key_material": &schema.Schema{
"key_material": {
Type: schema.TypeString,
Required: true,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SDC_KEY_MATERIAL", ""),
},
"key_id": &schema.Schema{
"key_id": {
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("SDC_KEY_ID", ""),
@ -53,70 +55,113 @@ func Provider() terraform.ResourceProvider {
}
}
type SDCConfig struct {
type Config struct {
Account string
KeyMaterial string
KeyID string
URL string
}
func (c SDCConfig) validate() error {
func (c Config) validate() error {
var err *multierror.Error
if c.URL == "" {
err = multierror.Append(err, fmt.Errorf("URL must be configured for the Triton provider"))
}
if c.KeyMaterial == "" {
err = multierror.Append(err, fmt.Errorf("Key Material must be configured for the Triton provider"))
err = multierror.Append(err, errors.New("URL must be configured for the Triton provider"))
}
if c.KeyID == "" {
err = multierror.Append(err, fmt.Errorf("Key ID must be configured for the Triton provider"))
err = multierror.Append(err, errors.New("Key ID must be configured for the Triton provider"))
}
if c.Account == "" {
err = multierror.Append(err, fmt.Errorf("Account must be configured for the Triton provider"))
err = multierror.Append(err, errors.New("Account must be configured for the Triton provider"))
}
return err.ErrorOrNil()
}
func (c SDCConfig) getSDCClient() (*cloudapi.Client, error) {
userauth, err := auth.NewAuth(c.Account, c.KeyMaterial, "rsa-sha256")
func (c Config) getTritonClient() (*triton.Client, error) {
var signer authentication.Signer
var err error
if c.KeyMaterial == "" {
signer, err = authentication.NewSSHAgentSigner(c.KeyID, c.Account)
if err != nil {
return nil, err
return nil, errwrap.Wrapf("Error Creating SSH Agent Signer: {{err}}", err)
}
} else {
signer, err = authentication.NewPrivateKeySigner(c.KeyID, []byte(c.KeyMaterial), c.Account)
if err != nil {
return nil, errwrap.Wrapf("Error Creating SSH Private Key Signer: {{err}}", err)
}
}
creds := &auth.Credentials{
UserAuthentication: userauth,
SdcKeyId: c.KeyID,
SdcEndpoint: auth.Endpoint{URL: c.URL},
client, err := triton.NewClient(c.URL, c.Account, signer)
if err != nil {
return nil, errwrap.Wrapf("Error Creating Triton Client: {{err}}", err)
}
client := cloudapi.New(client.NewClient(
c.URL,
cloudapi.DefaultAPIVersion,
creds,
log.New(os.Stderr, "", log.LstdFlags),
))
return client, nil
}
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := SDCConfig{
config := Config{
Account: d.Get("account").(string),
URL: d.Get("url").(string),
KeyMaterial: d.Get("key_material").(string),
KeyID: d.Get("key_id").(string),
}
if keyMaterial, ok := d.GetOk("key_material"); ok {
config.KeyMaterial = keyMaterial.(string)
}
if err := config.validate(); err != nil {
return nil, err
}
client, err := config.getSDCClient()
client, err := config.getTritonClient()
if err != nil {
return nil, err
}
return client, nil
}
func resourceExists(resource interface{}, err error) (bool, error) {
if err != nil {
if triton.IsResourceNotFound(err) {
return false, nil
}
return false, err
}
return resource != nil, nil
}
func stableMapHash(input map[string]string) string {
keys := make([]string, 0, len(input))
for k := range input {
keys = append(keys, k)
}
sort.Strings(keys)
hash := md5.New()
for _, key := range keys {
hash.Write([]byte(key))
hash.Write([]byte(input[key]))
}
return base64.StdEncoding.EncodeToString(hash.Sum([]byte{}))
}
var fastResourceTimeout = &schema.ResourceTimeout{
Create: schema.DefaultTimeout(1 * time.Minute),
Read: schema.DefaultTimeout(30 * time.Second),
Update: schema.DefaultTimeout(1 * time.Minute),
Delete: schema.DefaultTimeout(1 * time.Minute),
}
var slowResourceTimeout = &schema.ResourceTimeout{
Create: schema.DefaultTimeout(10 * time.Minute),
Read: schema.DefaultTimeout(30 * time.Second),
Update: schema.DefaultTimeout(10 * time.Minute),
Delete: schema.DefaultTimeout(10 * time.Minute),
}

View File

@ -32,13 +32,13 @@ func testAccPreCheck(t *testing.T) {
sdcURL := os.Getenv("SDC_URL")
account := os.Getenv("SDC_ACCOUNT")
keyID := os.Getenv("SDC_KEY_ID")
keyMaterial := os.Getenv("SDC_KEY_MATERIAL")
if sdcURL == "" {
sdcURL = "https://us-west-1.api.joyentcloud.com"
}
if sdcURL == "" || account == "" || keyID == "" || keyMaterial == "" {
t.Fatal("SDC_ACCOUNT, SDC_KEY_ID and SDC_KEY_MATERIAL must be set for acceptance tests")
if sdcURL == "" || account == "" || keyID == "" {
t.Fatal("SDC_ACCOUNT and SDC_KEY_ID must be set for acceptance tests. To test with the SSH" +
" private key signer, SDC_KEY_MATERIAL must also be set.")
}
}

View File

@ -4,7 +4,7 @@ import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
"github.com/joyent/gosdc/cloudapi"
"github.com/joyent/triton-go"
)
func resourceFabric() *schema.Resource {
@ -16,74 +16,74 @@ func resourceFabric() *schema.Resource {
Schema: map[string]*schema.Schema{
"name": {
Description: "network name",
Description: "Network name",
Required: true,
ForceNew: true,
Type: schema.TypeString,
},
"public": {
Description: "whether or not this is an RFC1918 network",
Description: "Whether or not this is an RFC1918 network",
Computed: true,
Type: schema.TypeBool,
},
"fabric": {
Description: "whether or not this network is on a fabric",
Description: "Whether or not this network is on a fabric",
Computed: true,
Type: schema.TypeBool,
},
"description": {
Description: "optional description of network",
Description: "Description of network",
Optional: true,
ForceNew: true,
Type: schema.TypeString,
},
"subnet": {
Description: "CIDR formatted string describing network",
Description: "CIDR formatted string describing network address space",
Required: true,
ForceNew: true,
Type: schema.TypeString,
},
"provision_start_ip": {
Description: "first IP on the network that can be assigned",
Description: "First IP on the network that can be assigned",
Required: true,
ForceNew: true,
Type: schema.TypeString,
},
"provision_end_ip": {
Description: "last assignable IP on the network",
Description: "Last assignable IP on the network",
Required: true,
ForceNew: true,
Type: schema.TypeString,
},
"gateway": {
Description: "optional gateway IP",
Description: "Gateway IP",
Optional: true,
ForceNew: true,
Type: schema.TypeString,
},
"resolvers": {
Description: "array of IP addresses for resolvers",
Description: "List of IP addresses for DNS resolvers",
Optional: true,
Computed: true,
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
},
"routes": {
Description: "map of CIDR block to Gateway IP address",
Description: "Map of CIDR block to Gateway IP address",
Computed: true,
Optional: true,
ForceNew: true,
Type: schema.TypeMap,
},
"internet_nat": {
Description: "if a NAT zone is provisioned at Gateway IP address",
Description: "Whether or not a NAT zone is provisioned at the Gateway IP address",
Computed: true,
Optional: true,
ForceNew: true,
Type: schema.TypeBool,
},
"vlan_id": {
Description: "VLAN network is on",
Description: "VLAN on which the network exists",
Required: true,
ForceNew: true,
Type: schema.TypeInt,
@ -93,7 +93,7 @@ func resourceFabric() *schema.Resource {
}
func resourceFabricCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
var resolvers []string
for _, resolver := range d.Get("resolvers").([]interface{}) {
@ -104,19 +104,18 @@ func resourceFabricCreate(d *schema.ResourceData, meta interface{}) error {
for cidr, v := range d.Get("routes").(map[string]interface{}) {
ip, ok := v.(string)
if !ok {
return fmt.Errorf(`cannot use "%v" as an IP address`, v)
return fmt.Errorf(`Cannot use "%v" as an IP address`, v)
}
routes[cidr] = ip
}
fabric, err := client.CreateFabricNetwork(
int16(d.Get("vlan_id").(int)),
cloudapi.CreateFabricNetworkOpts{
fabric, err := client.Fabrics().CreateFabricNetwork(&triton.CreateFabricNetworkInput{
FabricVLANID: d.Get("vlan_id").(int),
Name: d.Get("name").(string),
Description: d.Get("description").(string),
Subnet: d.Get("subnet").(string),
ProvisionStartIp: d.Get("provision_start_ip").(string),
ProvisionEndIp: d.Get("provision_end_ip").(string),
ProvisionStartIP: d.Get("provision_start_ip").(string),
ProvisionEndIP: d.Get("provision_end_ip").(string),
Gateway: d.Get("gateway").(string),
Resolvers: resolvers,
Routes: routes,
@ -129,26 +128,25 @@ func resourceFabricCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId(fabric.Id)
err = resourceFabricRead(d, meta)
if err != nil {
return err
}
return nil
return resourceFabricRead(d, meta)
}
func resourceFabricExists(d *schema.ResourceData, meta interface{}) (bool, error) {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
fabric, err := client.GetFabricNetwork(int16(d.Get("vlan_id").(int)), d.Id())
return fabric != nil && err == nil, err
return resourceExists(client.Fabrics().GetFabricNetwork(&triton.GetFabricNetworkInput{
FabricVLANID: d.Get("vlan_id").(int),
NetworkID: d.Id(),
}))
}
func resourceFabricRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
fabric, err := client.GetFabricNetwork(int16(d.Get("vlan_id").(int)), d.Id())
fabric, err := client.Fabrics().GetFabricNetwork(&triton.GetFabricNetworkInput{
FabricVLANID: d.Get("vlan_id").(int),
NetworkID: d.Id(),
})
if err != nil {
return err
}
@ -156,23 +154,25 @@ func resourceFabricRead(d *schema.ResourceData, meta interface{}) error {
d.SetId(fabric.Id)
d.Set("name", fabric.Name)
d.Set("public", fabric.Public)
d.Set("public", fabric.Public)
d.Set("fabric", fabric.Fabric)
d.Set("description", fabric.Description)
d.Set("subnet", fabric.Subnet)
d.Set("provision_start_ip", fabric.ProvisionStartIp)
d.Set("provision_end_ip", fabric.ProvisionEndIp)
d.Set("provision_start_ip", fabric.ProvisioningStartIP)
d.Set("provision_end_ip", fabric.ProvisioningEndIP)
d.Set("gateway", fabric.Gateway)
d.Set("resolvers", fabric.Resolvers)
d.Set("routes", fabric.Routes)
d.Set("internet_nat", fabric.InternetNAT)
d.Set("vlan_id", fabric.VLANId)
d.Set("vlan_id", d.Get("vlan_id").(int))
return nil
}
func resourceFabricDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
return client.DeleteFabricNetwork(int16(d.Get("vlan_id").(int)), d.Id())
return client.Fabrics().DeleteFabricNetwork(&triton.DeleteFabricNetworkInput{
FabricVLANID: d.Get("vlan_id").(int),
NetworkID: d.Id(),
})
}

View File

@ -9,19 +9,19 @@ import (
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/joyent/gosdc/cloudapi"
"github.com/joyent/triton-go"
)
func TestAccTritonFabric_basic(t *testing.T) {
fabricName := fmt.Sprintf("acctest-%d", acctest.RandInt())
config := fmt.Sprintf(testAccTritonFabric_basic, fabricName)
config := fmt.Sprintf(testAccTritonFabric_basic, acctest.RandIntRange(3, 2049), fabricName, fabricName)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckTritonFabricDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
testCheckTritonFabricExists("triton_fabric.test"),
@ -37,62 +37,75 @@ func TestAccTritonFabric_basic(t *testing.T) {
func testCheckTritonFabricExists(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
// Ensure we have enough information in state to look up in API
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Not found: %s", name)
}
conn := testAccProvider.Meta().(*cloudapi.Client)
conn := testAccProvider.Meta().(*triton.Client)
id, err := strconv.ParseInt(rs.Primary.Attributes["vlan_id"], 10, 16)
vlanID, err := strconv.Atoi(rs.Primary.Attributes["vlan_id"])
if err != nil {
return err
}
fabric, err := conn.GetFabricNetwork(int16(id), rs.Primary.ID)
exists, err := resourceExists(conn.Fabrics().GetFabricNetwork(&triton.GetFabricNetworkInput{
FabricVLANID: vlanID,
NetworkID: rs.Primary.ID,
}))
if err != nil {
return fmt.Errorf("Bad: Check Fabric Exists: %s", err)
}
if fabric == nil {
return fmt.Errorf("Bad: Fabric %q does not exist", rs.Primary.ID)
return fmt.Errorf("Error: Check Fabric Exists: %s", err)
}
if exists {
return nil
}
return fmt.Errorf("Error: Fabric %q (VLAN %d) Does Not Exist", rs.Primary.ID, vlanID)
}
}
func testCheckTritonFabricDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*cloudapi.Client)
conn := testAccProvider.Meta().(*triton.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "triton_fabric" {
continue
}
id, err := strconv.ParseInt(rs.Primary.Attributes["vlan_id"], 10, 16)
vlanID, err := strconv.Atoi(rs.Primary.Attributes["vlan_id"])
if err != nil {
return err
}
fabric, err := conn.GetFabricNetwork(int16(id), rs.Primary.ID)
exists, err := resourceExists(conn.Fabrics().GetFabricNetwork(&triton.GetFabricNetworkInput{
FabricVLANID: vlanID,
NetworkID: rs.Primary.ID,
}))
if err != nil {
return nil
}
if fabric != nil {
return fmt.Errorf("Bad: Fabric %q still exists", rs.Primary.ID)
if exists {
return fmt.Errorf("Error: Fabric %q (VLAN %d) Still Exists", rs.Primary.ID, vlanID)
}
return nil
}
return nil
}
var testAccTritonFabric_basic = `
resource "triton_vlan" "test" {
vlan_id = "%d"
name = "%s"
description = "testAccTritonFabric_basic"
}
resource "triton_fabric" "test" {
name = "%s"
description = "test network"
vlan_id = 2 # every DC seems to have a vlan 2 available
vlan_id = "${triton_vlan.test.id}"
subnet = "10.0.0.0/22"
gateway = "10.0.0.1"

View File

@ -2,8 +2,7 @@ package triton
import (
"github.com/hashicorp/terraform/helper/schema"
"github.com/joyent/gocommon/errors"
"github.com/joyent/gosdc/cloudapi"
"github.com/joyent/triton-go"
)
func resourceFirewallRule() *schema.Resource {
@ -14,7 +13,7 @@ func resourceFirewallRule() *schema.Resource {
Update: resourceFirewallRuleUpdate,
Delete: resourceFirewallRuleDelete,
Importer: &schema.ResourceImporter{
State: resourceFirewallRuleImporter,
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
@ -29,67 +28,73 @@ func resourceFirewallRule() *schema.Resource {
Optional: true,
Default: false,
},
"description": {
Description: "Human-readable description of the rule",
Type: schema.TypeString,
Optional: true,
},
"global": {
Description: "Indicates whether or not the rule is global",
Type: schema.TypeBool,
Computed: true,
},
},
}
}
func resourceFirewallRuleCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
rule, err := client.CreateFirewallRule(cloudapi.CreateFwRuleOpts{
rule, err := client.Firewall().CreateFirewallRule(&triton.CreateFirewallRuleInput{
Rule: d.Get("rule").(string),
Enabled: d.Get("enabled").(bool),
Description: d.Get("description").(string),
})
if err != nil {
return err
}
d.SetId(rule.Id)
d.SetId(rule.ID)
err = resourceFirewallRuleRead(d, meta)
if err != nil {
return err
}
return nil
return resourceFirewallRuleRead(d, meta)
}
func resourceFirewallRuleExists(d *schema.ResourceData, meta interface{}) (bool, error) {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
rule, err := client.GetFirewallRule(d.Id())
if errors.IsResourceNotFound(err) {
return false, nil
}
return rule != nil && err == nil, err
return resourceExists(client.Firewall().GetFirewallRule(&triton.GetFirewallRuleInput{
ID: d.Id(),
}))
}
func resourceFirewallRuleRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
rule, err := client.GetFirewallRule(d.Id())
rule, err := client.Firewall().GetFirewallRule(&triton.GetFirewallRuleInput{
ID: d.Id(),
})
if err != nil {
return err
}
d.SetId(rule.Id)
d.SetId(rule.ID)
d.Set("rule", rule.Rule)
d.Set("enabled", rule.Enabled)
d.Set("global", rule.Global)
d.Set("description", rule.Description)
return nil
}
func resourceFirewallRuleUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
_, err := client.UpdateFirewallRule(
d.Id(),
cloudapi.CreateFwRuleOpts{
_, err := client.Firewall().UpdateFirewallRule(&triton.UpdateFirewallRuleInput{
ID: d.Id(),
Rule: d.Get("rule").(string),
Enabled: d.Get("enabled").(bool),
},
)
Description: d.Get("description").(string),
})
if err != nil {
return err
}
@ -98,15 +103,9 @@ func resourceFirewallRuleUpdate(d *schema.ResourceData, meta interface{}) error
}
func resourceFirewallRuleDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
if err := client.DeleteFirewallRule(d.Id()); err != nil {
return err
}
return nil
}
func resourceFirewallRuleImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
return []*schema.ResourceData{d}, nil
return client.Firewall().DeleteFirewallRule(&triton.DeleteFirewallRuleInput{
ID: d.Id(),
})
}

View File

@ -6,7 +6,7 @@ import (
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/joyent/gosdc/cloudapi"
"github.com/joyent/triton-go"
)
func TestAccTritonFirewallRule_basic(t *testing.T) {
@ -17,7 +17,7 @@ func TestAccTritonFirewallRule_basic(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testCheckTritonFirewallRuleDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
testCheckTritonFirewallRuleExists("triton_firewall_rule.test"),
@ -36,20 +36,20 @@ func TestAccTritonFirewallRule_update(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testCheckTritonFirewallRuleDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: preConfig,
Check: resource.ComposeTestCheckFunc(
testCheckTritonFirewallRuleExists("triton_firewall_rule.test"),
resource.TestCheckResourceAttr("triton_firewall_rule.test", "rule", "FROM any TO tag www ALLOW tcp PORT 80"),
resource.TestCheckResourceAttr("triton_firewall_rule.test", "rule", "FROM any TO tag \"www\" ALLOW tcp PORT 80"),
resource.TestCheckResourceAttr("triton_firewall_rule.test", "enabled", "false"),
),
},
resource.TestStep{
{
Config: postConfig,
Check: resource.ComposeTestCheckFunc(
testCheckTritonFirewallRuleExists("triton_firewall_rule.test"),
resource.TestCheckResourceAttr("triton_firewall_rule.test", "rule", "FROM any TO tag www BLOCK tcp PORT 80"),
resource.TestCheckResourceAttr("triton_firewall_rule.test", "rule", "FROM any TO tag \"www\" BLOCK tcp PORT 80"),
resource.TestCheckResourceAttr("triton_firewall_rule.test", "enabled", "true"),
),
},
@ -66,20 +66,20 @@ func TestAccTritonFirewallRule_enable(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testCheckTritonFirewallRuleDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: preConfig,
Check: resource.ComposeTestCheckFunc(
testCheckTritonFirewallRuleExists("triton_firewall_rule.test"),
resource.TestCheckResourceAttr("triton_firewall_rule.test", "rule", "FROM any TO tag www ALLOW tcp PORT 80"),
resource.TestCheckResourceAttr("triton_firewall_rule.test", "rule", "FROM any TO tag \"www\" ALLOW tcp PORT 80"),
resource.TestCheckResourceAttr("triton_firewall_rule.test", "enabled", "false"),
),
},
resource.TestStep{
{
Config: postConfig,
Check: resource.ComposeTestCheckFunc(
testCheckTritonFirewallRuleExists("triton_firewall_rule.test"),
resource.TestCheckResourceAttr("triton_firewall_rule.test", "rule", "FROM any TO tag www ALLOW tcp PORT 80"),
resource.TestCheckResourceAttr("triton_firewall_rule.test", "rule", "FROM any TO tag \"www\" ALLOW tcp PORT 80"),
resource.TestCheckResourceAttr("triton_firewall_rule.test", "enabled", "true"),
),
},
@ -94,15 +94,19 @@ func testCheckTritonFirewallRuleExists(name string) resource.TestCheckFunc {
if !ok {
return fmt.Errorf("Not found: %s", name)
}
conn := testAccProvider.Meta().(*cloudapi.Client)
conn := testAccProvider.Meta().(*triton.Client)
rule, err := conn.GetFirewallRule(rs.Primary.ID)
if err != nil {
resp, err := conn.Firewall().GetFirewallRule(&triton.GetFirewallRuleInput{
ID: rs.Primary.ID,
})
if err != nil && triton.IsResourceNotFound(err) {
return fmt.Errorf("Bad: Check Firewall Rule Exists: %s", err)
} else if err != nil {
return err
}
if rule == nil {
return fmt.Errorf("Bad: Firewall rule %q does not exist", rs.Primary.ID)
if resp == nil {
return fmt.Errorf("Bad: Firewall Rule %q does not exist", rs.Primary.ID)
}
return nil
@ -110,20 +114,24 @@ func testCheckTritonFirewallRuleExists(name string) resource.TestCheckFunc {
}
func testCheckTritonFirewallRuleDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*cloudapi.Client)
conn := testAccProvider.Meta().(*triton.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "triton_firewall_rule" {
continue
}
resp, err := conn.GetFirewallRule(rs.Primary.ID)
if err != nil {
resp, err := conn.Firewall().GetFirewallRule(&triton.GetFirewallRuleInput{
ID: rs.Primary.ID,
})
if triton.IsResourceNotFound(err) {
return nil
} else if err != nil {
return err
}
if resp != nil {
return fmt.Errorf("Bad: Firewall rule %q still exists", rs.Primary.ID)
return fmt.Errorf("Bad: Firewall Rule %q still exists", rs.Primary.ID)
}
}
@ -132,21 +140,21 @@ func testCheckTritonFirewallRuleDestroy(s *terraform.State) error {
var testAccTritonFirewallRule_basic = `
resource "triton_firewall_rule" "test" {
rule = "FROM any TO tag www ALLOW tcp PORT 80"
rule = "FROM any TO tag \"www\" ALLOW tcp PORT 80"
enabled = false
}
`
var testAccTritonFirewallRule_update = `
resource "triton_firewall_rule" "test" {
rule = "FROM any TO tag www BLOCK tcp PORT 80"
rule = "FROM any TO tag \"www\" BLOCK tcp PORT 80"
enabled = true
}
`
var testAccTritonFirewallRule_enable = `
resource "triton_firewall_rule" "test" {
rule = "FROM any TO tag www ALLOW tcp PORT 80"
rule = "FROM any TO tag \"www\" ALLOW tcp PORT 80"
enabled = true
}
`

View File

@ -5,13 +5,7 @@ import (
"strings"
"github.com/hashicorp/terraform/helper/schema"
"github.com/joyent/gosdc/cloudapi"
)
var (
// ErrNoKeyComment will be returned when the key name cannot be generated from
// the key comment and is not otherwise specified.
ErrNoKeyComment = errors.New("no key comment found to use as a name (and none specified)")
"github.com/joyent/triton-go"
)
func resourceKey() *schema.Resource {
@ -20,20 +14,21 @@ func resourceKey() *schema.Resource {
Exists: resourceKeyExists,
Read: resourceKeyRead,
Delete: resourceKeyDelete,
Timeouts: fastResourceTimeout,
Importer: &schema.ResourceImporter{
State: resourceKeyImporter,
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Description: "name of this key (will be generated from the key comment, if not set and comment present)",
"name": {
Description: "Name of the key (generated from the key comment if not set)",
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"key": &schema.Schema{
Description: "content of public key from disk",
"key": {
Description: "Content of public key from disk in OpenSSH format",
Type: schema.TypeString,
Required: true,
ForceNew: true,
@ -43,18 +38,18 @@ func resourceKey() *schema.Resource {
}
func resourceKeyCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
if d.Get("name").(string) == "" {
if keyName := d.Get("name").(string); keyName == "" {
parts := strings.SplitN(d.Get("key").(string), " ", 3)
if len(parts) == 3 {
d.Set("name", parts[2])
} else {
return ErrNoKeyComment
return errors.New("No key name specified, and key material has no comment")
}
}
_, err := client.CreateKey(cloudapi.CreateKeyOpts{
_, err := client.Keys().CreateKey(&triton.CreateKeyInput{
Name: d.Get("name").(string),
Key: d.Get("key").(string),
})
@ -64,35 +59,28 @@ func resourceKeyCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId(d.Get("name").(string))
err = resourceKeyRead(d, meta)
if err != nil {
return err
}
return nil
return resourceKeyRead(d, meta)
}
func resourceKeyExists(d *schema.ResourceData, meta interface{}) (bool, error) {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
keys, err := client.ListKeys()
_, err := client.Keys().GetKey(&triton.GetKeyInput{
KeyName: d.Id(),
})
if err != nil {
return false, err
}
for _, key := range keys {
if key.Name == d.Id() {
return true, nil
}
}
return false, nil
}
func resourceKeyRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
key, err := client.GetKey(d.Id())
key, err := client.Keys().GetKey(&triton.GetKeyInput{
KeyName: d.Id(),
})
if err != nil {
return err
}
@ -104,15 +92,9 @@ func resourceKeyRead(d *schema.ResourceData, meta interface{}) error {
}
func resourceKeyDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
if err := client.DeleteKey(d.Get("name").(string)); err != nil {
return err
}
return nil
}
func resourceKeyImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
return []*schema.ResourceData{d}, nil
return client.Keys().DeleteKey(&triton.DeleteKeyInput{
KeyName: d.Id(),
})
}

View File

@ -8,22 +8,57 @@ import (
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/joyent/gosdc/cloudapi"
"github.com/joyent/triton-go"
)
func TestAccTritonKey_basic(t *testing.T) {
keyName := fmt.Sprintf("acctest-%d", acctest.RandInt())
config := fmt.Sprintf(testAccTritonKey_basic, keyName, testAccTritonKey_basicMaterial)
publicKeyMaterial, _, err := acctest.RandSSHKeyPair("TestAccTritonKey_basic@terraform")
if err != nil {
t.Fatalf("Cannot generate test SSH key pair: %s", err)
}
config := testAccTritonKey_basic(keyName, publicKeyMaterial)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckTritonKeyDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
testCheckTritonKeyExists("triton_key.test"),
resource.TestCheckResourceAttr("triton_key.test", "name", keyName),
resource.TestCheckResourceAttr("triton_key.test", "key", publicKeyMaterial),
func(*terraform.State) error {
time.Sleep(10 * time.Second)
return nil
},
),
},
},
})
}
func TestAccTritonKey_noKeyName(t *testing.T) {
keyComment := fmt.Sprintf("acctest_%d@terraform", acctest.RandInt())
keyMaterial, _, err := acctest.RandSSHKeyPair(keyComment)
if err != nil {
t.Fatalf("Cannot generate test SSH key pair: %s", err)
}
config := testAccTritonKey_noKeyName(keyMaterial)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckTritonKeyDestroy,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
testCheckTritonKeyExists("triton_key.test"),
resource.TestCheckResourceAttr("triton_key.test", "name", keyComment),
resource.TestCheckResourceAttr("triton_key.test", "key", keyMaterial),
func(*terraform.State) error {
time.Sleep(10 * time.Second)
return nil
@ -41,14 +76,16 @@ func testCheckTritonKeyExists(name string) resource.TestCheckFunc {
if !ok {
return fmt.Errorf("Not found: %s", name)
}
conn := testAccProvider.Meta().(*cloudapi.Client)
conn := testAccProvider.Meta().(*triton.Client)
rule, err := conn.GetKey(rs.Primary.ID)
key, err := conn.Keys().GetKey(&triton.GetKeyInput{
KeyName: rs.Primary.ID,
})
if err != nil {
return fmt.Errorf("Bad: Check Key Exists: %s", err)
}
if rule == nil {
if key == nil {
return fmt.Errorf("Bad: Key %q does not exist", rs.Primary.ID)
}
@ -57,7 +94,7 @@ func testCheckTritonKeyExists(name string) resource.TestCheckFunc {
}
func testCheckTritonKeyDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*cloudapi.Client)
conn := testAccProvider.Meta().(*triton.Client)
return resource.Retry(1*time.Minute, func() *resource.RetryError {
for _, rs := range s.RootModule().Resources {
@ -65,12 +102,14 @@ func testCheckTritonKeyDestroy(s *terraform.State) error {
continue
}
resp, err := conn.GetKey(rs.Primary.ID)
key, err := conn.Keys().GetKey(&triton.GetKeyInput{
KeyName: rs.Primary.ID,
})
if err != nil {
return nil
}
if resp != nil {
if key != nil {
return resource.RetryableError(fmt.Errorf("Bad: Key %q still exists", rs.Primary.ID))
}
}
@ -79,11 +118,17 @@ func testCheckTritonKeyDestroy(s *terraform.State) error {
})
}
var testAccTritonKey_basic = `
resource "triton_key" "test" {
var testAccTritonKey_basic = func(keyName string, keyMaterial string) string {
return fmt.Sprintf(`resource "triton_key" "test" {
name = "%s"
key = "%s"
}
`
`, keyName, keyMaterial)
}
const testAccTritonKey_basicMaterial = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDL18KJIe8N7FxcgOMtabo10qZEDyYUSlOpsh/EYrugQCQHMKuNytog1lhFNZNk4LGNAz5L8/btG9+/axY/PfundbjR3SXt0hupAGQIVHuygWTr7foj5iGhckrEM+r3eMCXqoCnIFLhDZLDcq/zN2MxNbqDKcWSYmc8ul9dZWuiQpKOL+0nNXjhYA8Ewu+07kVAtsZD0WfvnAUjxmYb3rB15eBWk7gLxHrOPfZpeDSvOOX2bmzikpLn+L5NKrJsLrzO6hU/rpxD4OTHLULcsnIts3lYH8hShU8uY5ry94PBzdix++se3pUGvNSe967fKlHw3Ymh9nE/LJDQnzTNyFMj James@jn-mpb13`
var testAccTritonKey_noKeyName = func(keyMaterial string) string {
return fmt.Sprintf(`resource "triton_key" "test" {
key = "%s"
}
`, keyMaterial)
}

View File

@ -2,22 +2,20 @@ package triton
import (
"fmt"
"reflect"
"regexp"
"time"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/joyent/gosdc/cloudapi"
"github.com/joyent/triton-go"
)
var (
machineStateRunning = "running"
machineStateStopped = "stopped"
machineStateDeleted = "deleted"
machineStateChangeTimeout = 10 * time.Minute
machineStateChangeCheckInterval = 10 * time.Second
resourceMachineMetadataKeys = map[string]string{
// semantics: "schema_name": "metadata_name"
@ -35,45 +33,41 @@ func resourceMachine() *schema.Resource {
Read: resourceMachineRead,
Update: resourceMachineUpdate,
Delete: resourceMachineDelete,
Timeouts: slowResourceTimeout,
Importer: &schema.ResourceImporter{
State: resourceMachineImporter,
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"name": {
Description: "friendly name",
Description: "Friendly name for machine",
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: resourceMachineValidateName,
},
"type": {
Description: "machine type (smartmachine or virtualmachine)",
Type: schema.TypeString,
Computed: true,
},
"state": {
Description: "current state of the machine",
Description: "Machine type (smartmachine or virtualmachine)",
Type: schema.TypeString,
Computed: true,
},
"dataset": {
Description: "dataset URN the machine was provisioned with",
Description: "Dataset URN with which the machine was provisioned",
Type: schema.TypeString,
Computed: true,
},
"memory": {
Description: "amount of memory the machine has (in Mb)",
Description: "Amount of memory allocated to the machine (in Mb)",
Type: schema.TypeInt,
Computed: true,
},
"disk": {
Description: "amount of disk the machine has (in Gb)",
Description: "Amount of disk allocated to the machine (in Gb)",
Type: schema.TypeInt,
Computed: true,
},
"ips": {
Description: "IP addresses the machine has",
Description: "IP addresses assigned to the machine",
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{
@ -81,39 +75,38 @@ func resourceMachine() *schema.Resource {
},
},
"tags": {
Description: "machine tags",
Description: "Machine tags",
Type: schema.TypeMap,
Optional: true,
},
"created": {
Description: "when the machine was created",
Description: "When the machine was created",
Type: schema.TypeString,
Computed: true,
},
"updated": {
Description: "when the machine was update",
Description: "When the machine was updated",
Type: schema.TypeString,
Computed: true,
},
"package": {
Description: "name of the package to use on provisioning",
Description: "The package for use for provisioning",
Type: schema.TypeString,
Required: true,
},
"image": {
Description: "image UUID",
Description: "UUID of the image",
Type: schema.TypeString,
Required: true,
ForceNew: true,
// TODO: validate that the UUID is valid
},
"primaryip": {
Description: "the primary (public) IP address for the machine",
Description: "Primary (public) IP address for the machine",
Type: schema.TypeString,
Computed: true,
},
"nic": {
Description: "network interface",
Description: "Network interface",
Type: schema.TypeSet,
Computed: true,
Optional: true,
@ -148,27 +141,27 @@ func resourceMachine() *schema.Resource {
Computed: true,
Type: schema.TypeString,
},
"state": {
Description: "describes the state of the NIC (e.g. provisioning, running, or stopped)",
Computed: true,
"network": {
Description: "ID of the network to which the NIC is attached",
Required: true,
Type: schema.TypeString,
},
"network": {
Description: "Network ID this NIC is attached to",
Required: true,
"state": {
Description: "Provisioning state of the NIC",
Computed: true,
Type: schema.TypeString,
},
},
},
},
"firewall_enabled": {
Description: "enable firewall for this machine",
Description: "Whether to enable the firewall for this machine",
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"domain_names": {
Description: "list of domain names from Triton's CNS",
Description: "List of domain names from Triton CNS",
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{
@ -178,25 +171,25 @@ func resourceMachine() *schema.Resource {
// computed resources from metadata
"root_authorized_keys": {
Description: "authorized keys for the root user on this machine",
Description: "Authorized keys for the root user on this machine",
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"user_script": {
Description: "user script to run on boot (every boot on SmartMachines)",
Description: "User script to run on boot (every boot on SmartMachines)",
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"user_data": {
Description: "copied to machine on boot",
Description: "Data copied to machine on boot",
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"administrator_pw": {
Description: "administrator's initial password (Windows only)",
Description: "Administrator's initial password (Windows only)",
Type: schema.TypeString,
Optional: true,
Computed: true,
@ -204,7 +197,7 @@ func resourceMachine() *schema.Resource {
// deprecated fields
"networks": {
Description: "desired network IDs",
Description: "Desired network IDs",
Type: schema.TypeList,
Optional: true,
Computed: true,
@ -218,7 +211,7 @@ func resourceMachine() *schema.Resource {
}
func resourceMachineCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
var networks []string
for _, network := range d.Get("networks").([]interface{}) {
@ -242,7 +235,7 @@ func resourceMachineCreate(d *schema.ResourceData, meta interface{}) error {
tags[k] = v.(string)
}
machine, err := client.CreateMachine(cloudapi.CreateMachineOpts{
machine, err := client.Machines().CreateMachine(&triton.CreateMachineInput{
Name: d.Get("name").(string),
Package: d.Get("package").(string),
Image: d.Get("image").(string),
@ -255,47 +248,64 @@ func resourceMachineCreate(d *schema.ResourceData, meta interface{}) error {
return err
}
err = waitForMachineState(client, machine.Id, machineStateRunning, machineStateChangeTimeout)
d.SetId(machine.ID)
stateConf := &resource.StateChangeConf{
Target: []string{fmt.Sprintf(machineStateRunning)},
Refresh: func() (interface{}, string, error) {
getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{
ID: d.Id(),
})
if err != nil {
return nil, "", err
}
return getResp, getResp.State, nil
},
Timeout: machineStateChangeTimeout,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return err
}
if err != nil {
return err
}
// refresh state after it provisions
d.SetId(machine.Id)
err = resourceMachineRead(d, meta)
if err != nil {
return err
}
return nil
return resourceMachineRead(d, meta)
}
func resourceMachineExists(d *schema.ResourceData, meta interface{}) (bool, error) {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
machine, err := client.GetMachine(d.Id())
return machine != nil && err == nil, err
return resourceExists(client.Machines().GetMachine(&triton.GetMachineInput{
ID: d.Id(),
}))
}
func resourceMachineRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
machine, err := client.GetMachine(d.Id())
machine, err := client.Machines().GetMachine(&triton.GetMachineInput{
ID: d.Id(),
})
if err != nil {
return err
}
nics, err := client.ListNICs(d.Id())
nics, err := client.Machines().ListNICs(&triton.ListNICsInput{
MachineID: d.Id(),
})
if err != nil {
return err
}
d.SetId(machine.Id)
d.Set("name", machine.Name)
d.Set("type", machine.Type)
d.Set("state", machine.State)
d.Set("dataset", machine.Dataset)
d.Set("dataset", machine.Image)
d.Set("image", machine.Image)
d.Set("memory", machine.Memory)
d.Set("disk", machine.Disk)
d.Set("ips", machine.IPs)
@ -340,23 +350,40 @@ func resourceMachineRead(d *schema.ResourceData, meta interface{}) error {
}
func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
d.Partial(true)
if d.HasChange("name") {
if err := client.RenameMachine(d.Id(), d.Get("name").(string)); err != nil {
oldNameInterface, newNameInterface := d.GetChange("name")
oldName := oldNameInterface.(string)
newName := newNameInterface.(string)
err := client.Machines().RenameMachine(&triton.RenameMachineInput{
ID: d.Id(),
Name: newName,
})
if err != nil {
return err
}
err := waitFor(
func() (bool, error) {
machine, err := client.GetMachine(d.Id())
return machine.Name == d.Get("name").(string), err
stateConf := &resource.StateChangeConf{
Pending: []string{oldName},
Target: []string{newName},
Refresh: func() (interface{}, string, error) {
getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{
ID: d.Id(),
})
if err != nil {
return nil, "", err
}
return getResp, getResp.Name, nil
},
machineStateChangeCheckInterval,
1*time.Minute,
)
Timeout: machineStateChangeTimeout,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return err
}
@ -372,22 +399,36 @@ func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error {
var err error
if len(tags) == 0 {
err = client.DeleteMachineTags(d.Id())
err = client.Machines().DeleteMachineTags(&triton.DeleteMachineTagsInput{
ID: d.Id(),
})
} else {
_, err = client.ReplaceMachineTags(d.Id(), tags)
err = client.Machines().ReplaceMachineTags(&triton.ReplaceMachineTagsInput{
ID: d.Id(),
Tags: tags,
})
}
if err != nil {
return err
}
err = waitFor(
func() (bool, error) {
machine, err := client.GetMachine(d.Id())
return reflect.DeepEqual(machine.Tags, tags), err
expectedTagsMD5 := stableMapHash(tags)
stateConf := &resource.StateChangeConf{
Target: []string{expectedTagsMD5},
Refresh: func() (interface{}, string, error) {
getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{
ID: d.Id(),
})
if err != nil {
return nil, "", err
}
return getResp, stableMapHash(getResp.Tags), nil
},
machineStateChangeCheckInterval,
1*time.Minute,
)
Timeout: machineStateChangeTimeout,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return err
}
@ -396,18 +437,32 @@ func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error {
}
if d.HasChange("package") {
if err := client.ResizeMachine(d.Id(), d.Get("package").(string)); err != nil {
newPackage := d.Get("package").(string)
err := client.Machines().ResizeMachine(&triton.ResizeMachineInput{
ID: d.Id(),
Package: newPackage,
})
if err != nil {
return err
}
err := waitFor(
func() (bool, error) {
machine, err := client.GetMachine(d.Id())
return machine.Package == d.Get("package").(string) && machine.State == machineStateRunning, err
stateConf := &resource.StateChangeConf{
Target: []string{fmt.Sprintf("%s@%s", newPackage, "running")},
Refresh: func() (interface{}, string, error) {
getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{
ID: d.Id(),
})
if err != nil {
return nil, "", err
}
return getResp, fmt.Sprintf("%s@%s", getResp.Package, getResp.State), nil
},
machineStateChangeCheckInterval,
machineStateChangeTimeout,
)
Timeout: machineStateChangeTimeout,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return err
}
@ -416,25 +471,38 @@ func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error {
}
if d.HasChange("firewall_enabled") {
enable := d.Get("firewall_enabled").(bool)
var err error
if d.Get("firewall_enabled").(bool) {
err = client.EnableFirewallMachine(d.Id())
if enable {
err = client.Machines().EnableMachineFirewall(&triton.EnableMachineFirewallInput{
ID: d.Id(),
})
} else {
err = client.DisableFirewallMachine(d.Id())
err = client.Machines().DisableMachineFirewall(&triton.DisableMachineFirewallInput{
ID: d.Id(),
})
}
if err != nil {
return err
}
err = waitFor(
func() (bool, error) {
machine, err := client.GetMachine(d.Id())
return machine.FirewallEnabled == d.Get("firewall_enabled").(bool), err
},
machineStateChangeCheckInterval,
machineStateChangeTimeout,
)
stateConf := &resource.StateChangeConf{
Target: []string{fmt.Sprintf("%t", enable)},
Refresh: func() (interface{}, string, error) {
getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{
ID: d.Id(),
})
if err != nil {
return nil, "", err
}
return getResp, fmt.Sprintf("%t", getResp.FirewallEnabled), nil
},
Timeout: machineStateChangeTimeout,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return err
}
@ -452,24 +520,24 @@ func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error {
}
oldNICs := o.(*schema.Set)
newNICs := o.(*schema.Set)
newNICs := n.(*schema.Set)
// add new NICs that are not in old NICs
for _, nicI := range newNICs.Difference(oldNICs).List() {
nic := nicI.(map[string]interface{})
fmt.Printf("adding %+v\n", nic)
_, err := client.AddNIC(d.Id(), nic["network"].(string))
if err != nil {
if _, err := client.Machines().AddNIC(&triton.AddNICInput{
MachineID: d.Id(),
Network: nic["network"].(string),
}); err != nil {
return err
}
}
// remove old NICs that are not in new NICs
for _, nicI := range oldNICs.Difference(newNICs).List() {
nic := nicI.(map[string]interface{})
fmt.Printf("removing %+v\n", nic)
err := client.RemoveNIC(d.Id(), nic["mac"].(string))
if err != nil {
if err := client.Machines().RemoveNIC(&triton.RemoveNICInput{
MachineID: d.Id(),
MAC: nic["mac"].(string),
}); err != nil {
return err
}
}
@ -477,7 +545,6 @@ func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error {
d.SetPartial("nic")
}
// metadata stuff
metadata := map[string]string{}
for schemaName, metadataKey := range resourceMachineMetadataKeys {
if d.HasChange(schemaName) {
@ -485,24 +552,35 @@ func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error {
}
}
if len(metadata) > 0 {
_, err := client.UpdateMachineMetadata(d.Id(), metadata)
if err != nil {
if _, err := client.Machines().UpdateMachineMetadata(&triton.UpdateMachineMetadataInput{
ID: d.Id(),
Metadata: metadata,
}); err != nil {
return err
}
err = waitFor(
func() (bool, error) {
machine, err := client.GetMachine(d.Id())
stateConf := &resource.StateChangeConf{
Target: []string{"converged"},
Refresh: func() (interface{}, string, error) {
getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{
ID: d.Id(),
})
if err != nil {
return nil, "", err
}
for k, v := range metadata {
if provider_v, ok := machine.Metadata[k]; !ok || v != provider_v {
return false, err
if upstream, ok := getResp.Metadata[k]; !ok || v != upstream {
return getResp, "converging", nil
}
}
return true, err
return getResp, "converged", nil
},
machineStateChangeCheckInterval,
1*time.Minute,
)
Timeout: machineStateChangeTimeout,
MinTimeout: 3 * time.Second,
}
_, err := stateConf.WaitForState()
if err != nil {
return err
}
@ -516,57 +594,43 @@ func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error {
d.Partial(false)
err := resourceMachineRead(d, meta)
if err != nil {
return err
}
return nil
return resourceMachineRead(d, meta)
}
func resourceMachineDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
state, err := readMachineState(client, d.Id())
if state != machineStateStopped {
err = client.StopMachine(d.Id())
err := client.Machines().DeleteMachine(&triton.DeleteMachineInput{
ID: d.Id(),
})
if err != nil {
return err
}
waitForMachineState(client, d.Id(), machineStateStopped, machineStateChangeTimeout)
}
err = client.DeleteMachine(d.Id())
stateConf := &resource.StateChangeConf{
Target: []string{machineStateDeleted},
Refresh: func() (interface{}, string, error) {
getResp, err := client.Machines().GetMachine(&triton.GetMachineInput{
ID: d.Id(),
})
if err != nil {
return err
if triton.IsResourceNotFound(err) {
return nil, "deleted", nil
}
return nil, "", err
}
waitForMachineState(client, d.Id(), machineStateDeleted, machineStateChangeTimeout)
return nil
}
func readMachineState(api *cloudapi.Client, id string) (string, error) {
machine, err := api.GetMachine(id)
if err != nil {
return "", err
}
return machine.State, nil
}
// waitForMachineState waits for a machine to be in the desired state (waiting
// some seconds between each poll). If it doesn't reach the state within the
// duration specified in `timeout`, it returns ErrMachineStateTimeout.
func waitForMachineState(api *cloudapi.Client, id, state string, timeout time.Duration) error {
return waitFor(
func() (bool, error) {
currentState, err := readMachineState(api, id)
return currentState == state, err
return getResp, getResp.State, nil
},
machineStateChangeCheckInterval,
machineStateChangeTimeout,
)
Timeout: machineStateChangeTimeout,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return err
}
return nil
}
func resourceMachineValidateName(value interface{}, name string) (warnings []string, errors []error) {
@ -580,7 +644,3 @@ func resourceMachineValidateName(value interface{}, name string) (warnings []str
return warnings, errors
}
func resourceMachineImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
return []*schema.ResourceData{d}, nil
}

View File

@ -2,14 +2,16 @@ package triton
import (
"fmt"
"log"
"regexp"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/joyent/gosdc/cloudapi"
"github.com/joyent/triton-go"
)
func TestAccTritonMachine_basic(t *testing.T) {
@ -21,7 +23,7 @@ func TestAccTritonMachine_basic(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testCheckTritonMachineDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
testCheckTritonMachineExists("triton_machine.test"),
@ -44,20 +46,16 @@ func TestAccTritonMachine_dns(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testCheckTritonMachineDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: dns_output,
Check: resource.ComposeTestCheckFunc(
testCheckTritonMachineExists("triton_machine.test"),
func(*terraform.State) error {
func(state *terraform.State) error {
time.Sleep(10 * time.Second)
log.Printf("[DEBUG] %s", spew.Sdump(state))
return nil
},
),
},
resource.TestStep{
Config: dns_output,
Check: resource.TestMatchOutput(
"domain_names", regexp.MustCompile(".*acctest-.*"),
resource.TestMatchOutput("domain_names", regexp.MustCompile(".*acctest-.*")),
),
},
},
@ -66,14 +64,14 @@ func TestAccTritonMachine_dns(t *testing.T) {
func TestAccTritonMachine_nic(t *testing.T) {
machineName := fmt.Sprintf("acctest-%d", acctest.RandInt())
config := fmt.Sprintf(testAccTritonMachine_withnic, machineName, machineName)
config := testAccTritonMachine_singleNIC(machineName, acctest.RandIntRange(1024, 2048))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckTritonMachineDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
testCheckTritonMachineExists("triton_machine.test"),
@ -88,32 +86,33 @@ func TestAccTritonMachine_nic(t *testing.T) {
})
}
func TestAccTritonMachine_addnic(t *testing.T) {
func TestAccTritonMachine_addNIC(t *testing.T) {
machineName := fmt.Sprintf("acctest-%d", acctest.RandInt())
without := fmt.Sprintf(testAccTritonMachine_withoutnic, machineName, machineName)
with := fmt.Sprintf(testAccTritonMachine_withnic, machineName, machineName)
vlanNumber := acctest.RandIntRange(1024, 2048)
singleNICConfig := testAccTritonMachine_singleNIC(machineName, vlanNumber)
dualNICConfig := testAccTritonMachine_dualNIC(machineName, vlanNumber)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckTritonMachineDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: without,
{
Config: singleNICConfig,
Check: resource.ComposeTestCheckFunc(
testCheckTritonMachineExists("triton_machine.test"),
func(*terraform.State) error {
time.Sleep(10 * time.Second)
return nil
},
testCheckTritonMachineHasNoFabric("triton_machine.test", "triton_fabric.test"),
),
},
resource.TestStep{
Config: with,
{
Config: dualNICConfig,
Check: resource.ComposeTestCheckFunc(
testCheckTritonMachineExists("triton_machine.test"),
testCheckTritonMachineHasFabric("triton_machine.test", "triton_fabric.test"),
testCheckTritonMachineHasFabric("triton_machine.test", "triton_fabric.test_add"),
),
},
},
@ -127,14 +126,16 @@ func testCheckTritonMachineExists(name string) resource.TestCheckFunc {
if !ok {
return fmt.Errorf("Not found: %s", name)
}
conn := testAccProvider.Meta().(*cloudapi.Client)
conn := testAccProvider.Meta().(*triton.Client)
rule, err := conn.GetMachine(rs.Primary.ID)
machine, err := conn.Machines().GetMachine(&triton.GetMachineInput{
ID: rs.Primary.ID,
})
if err != nil {
return fmt.Errorf("Bad: Check Machine Exists: %s", err)
}
if rule == nil {
if machine == nil {
return fmt.Errorf("Bad: Machine %q does not exist", rs.Primary.ID)
}
@ -154,9 +155,11 @@ func testCheckTritonMachineHasFabric(name, fabricName string) resource.TestCheck
if !ok {
return fmt.Errorf("Not found: %s", fabricName)
}
conn := testAccProvider.Meta().(*cloudapi.Client)
conn := testAccProvider.Meta().(*triton.Client)
nics, err := conn.ListNICs(machine.Primary.ID)
nics, err := conn.Machines().ListNICs(&triton.ListNICsInput{
MachineID: machine.Primary.ID,
})
if err != nil {
return fmt.Errorf("Bad: Check NICs Exist: %s", err)
}
@ -171,49 +174,25 @@ func testCheckTritonMachineHasFabric(name, fabricName string) resource.TestCheck
}
}
func testCheckTritonMachineHasNoFabric(name, fabricName string) resource.TestCheckFunc {
return func(s *terraform.State) error {
// Ensure we have enough information in state to look up in API
machine, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Not found: %s", name)
}
network, ok := s.RootModule().Resources[fabricName]
if !ok {
return fmt.Errorf("Not found: %s", fabricName)
}
conn := testAccProvider.Meta().(*cloudapi.Client)
nics, err := conn.ListNICs(machine.Primary.ID)
if err != nil {
return fmt.Errorf("Bad: Check NICs Exist: %s", err)
}
for _, nic := range nics {
if nic.Network == network.Primary.ID {
return fmt.Errorf("Bad: Machine %q has Fabric %q", machine.Primary.ID, network.Primary.ID)
}
}
return nil
}
}
func testCheckTritonMachineDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*cloudapi.Client)
conn := testAccProvider.Meta().(*triton.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "triton_machine" {
continue
}
resp, err := conn.GetMachine(rs.Primary.ID)
resp, err := conn.Machines().GetMachine(&triton.GetMachineInput{
ID: rs.Primary.ID,
})
if err != nil {
if triton.IsResourceNotFound(err) {
return nil
}
return err
}
if resp != nil {
if resp != nil && resp.State != machineStateDeleted {
return fmt.Errorf("Bad: Machine %q still exists", rs.Primary.ID)
}
}
@ -231,7 +210,7 @@ func TestAccTritonMachine_firewall(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testCheckTritonMachineDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: enabled_config,
Check: resource.ComposeTestCheckFunc(
testCheckTritonMachineExists("triton_machine.test"),
@ -239,7 +218,7 @@ func TestAccTritonMachine_firewall(t *testing.T) {
"triton_machine.test", "firewall_enabled", "true"),
),
},
resource.TestStep{
{
Config: disabled_config,
Check: resource.ComposeTestCheckFunc(
testCheckTritonMachineExists("triton_machine.test"),
@ -247,7 +226,7 @@ func TestAccTritonMachine_firewall(t *testing.T) {
"triton_machine.test", "firewall_enabled", "false"),
),
},
resource.TestStep{
{
Config: enabled_config,
Check: resource.ComposeTestCheckFunc(
testCheckTritonMachineExists("triton_machine.test"),
@ -271,13 +250,13 @@ func TestAccTritonMachine_metadata(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testCheckTritonMachineDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: basic,
Check: resource.ComposeTestCheckFunc(
testCheckTritonMachineExists("triton_machine.test"),
),
},
resource.TestStep{
{
Config: add_metadata,
Check: resource.ComposeTestCheckFunc(
testCheckTritonMachineExists("triton_machine.test"),
@ -285,7 +264,7 @@ func TestAccTritonMachine_metadata(t *testing.T) {
"triton_machine.test", "user_data", "hello"),
),
},
resource.TestStep{
{
Config: add_metadata_2,
Check: resource.ComposeTestCheckFunc(
testCheckTritonMachineExists("triton_machine.test"),
@ -294,7 +273,7 @@ func TestAccTritonMachine_metadata(t *testing.T) {
"tags.triton.cns.services", "test-cns-service"),
),
},
resource.TestStep{
{
Config: add_metadata_3,
Check: resource.ComposeTestCheckFunc(
testCheckTritonMachineExists("triton_machine.test"),
@ -311,7 +290,7 @@ var testAccTritonMachine_basic = `
resource "triton_machine" "test" {
name = "%s"
package = "g4-general-4G"
image = "c20b4b7c-e1a6-11e5-9a4d-ef590901732e"
image = "fb5fe970-e6e4-11e6-9820-4b51be190db9"
tags = {
test = "hello!"
@ -332,7 +311,7 @@ var testAccTritonMachine_firewall_1 = `
resource "triton_machine" "test" {
name = "%s"
package = "g4-general-4G"
image = "c20b4b7c-e1a6-11e5-9a4d-ef590901732e"
image = "fb5fe970-e6e4-11e6-9820-4b51be190db9"
firewall_enabled = 1
}
@ -361,7 +340,7 @@ variable "tags" {
resource "triton_machine" "test" {
name = "%s"
package = "g4-highcpu-128M"
image = "c20b4b7c-e1a6-11e5-9a4d-ef590901732e"
image = "fb5fe970-e6e4-11e6-9820-4b51be190db9"
user_data = "hello"
@ -372,7 +351,7 @@ var testAccTritonMachine_metadata_3 = `
resource "triton_machine" "test" {
name = "%s"
package = "g4-highcpu-128M"
image = "c20b4b7c-e1a6-11e5-9a4d-ef590901732e"
image = "fb5fe970-e6e4-11e6-9820-4b51be190db9"
user_data = "hello"
@ -382,57 +361,91 @@ resource "triton_machine" "test" {
}
}
`
var testAccTritonMachine_withnic = `
var testAccTritonMachine_singleNIC = func(name string, vlanNumber int) string {
return fmt.Sprintf(`resource "triton_vlan" "test" {
vlan_id = %d
name = "%s-vlan"
description = "test vlan"
}
resource "triton_fabric" "test" {
name = "%s-network"
description = "test network"
vlan_id = 2 # every DC seems to have a vlan 2 available
vlan_id = "${triton_vlan.test.vlan_id}"
subnet = "10.0.0.0/22"
gateway = "10.0.0.1"
provision_start_ip = "10.0.0.5"
provision_end_ip = "10.0.3.250"
subnet = "10.10.0.0/24"
gateway = "10.10.0.1"
provision_start_ip = "10.10.0.10"
provision_end_ip = "10.10.0.250"
resolvers = ["8.8.8.8", "8.8.4.4"]
}
resource "triton_machine" "test" {
name = "%s"
package = "g4-general-4G"
image = "842e6fa6-6e9b-11e5-8402-1b490459e334"
name = "%s-instance"
package = "g4-highcpu-128M"
image = "fb5fe970-e6e4-11e6-9820-4b51be190db9"
tags = {
test = "hello!"
test = "Test"
}
nic { network = "${triton_fabric.test.id}" }
nic {
network = "${triton_fabric.test.id}"
}
}`, vlanNumber, name, name, name)
}
var testAccTritonMachine_dualNIC = func(name string, vlanNumber int) string {
return fmt.Sprintf(`resource "triton_vlan" "test" {
vlan_id = %d
name = "%s-vlan"
description = "test vlan"
}
`
var testAccTritonMachine_withoutnic = `
resource "triton_fabric" "test" {
name = "%s-network"
description = "test network"
vlan_id = 2 # every DC seems to have a vlan 2 available
vlan_id = "${triton_vlan.test.vlan_id}"
subnet = "10.0.0.0/22"
gateway = "10.0.0.1"
provision_start_ip = "10.0.0.5"
provision_end_ip = "10.0.3.250"
subnet = "10.10.0.0/24"
gateway = "10.10.0.1"
provision_start_ip = "10.10.0.10"
provision_end_ip = "10.10.0.250"
resolvers = ["8.8.8.8", "8.8.4.4"]
}
resource "triton_fabric" "test_add" {
name = "%s-network-2"
description = "test network 2"
vlan_id = "${triton_vlan.test.vlan_id}"
subnet = "172.23.0.0/24"
gateway = "172.23.0.1"
provision_start_ip = "172.23.0.10"
provision_end_ip = "172.23.0.250"
resolvers = ["8.8.8.8", "8.8.4.4"]
}
resource "triton_machine" "test" {
name = "%s"
package = "g4-general-4G"
image = "842e6fa6-6e9b-11e5-8402-1b490459e334"
name = "%s-instance"
package = "g4-highcpu-128M"
image = "fb5fe970-e6e4-11e6-9820-4b51be190db9"
tags = {
test = "hello!"
test = "Test"
}
nic {
network = "${triton_fabric.test.id}"
}
nic {
network = "${triton_fabric.test_add.id}"
}
}`, vlanNumber, name, name, name, name)
}
`
var testAccTritonMachine_dns = `
provider "triton" {
@ -441,8 +454,9 @@ provider "triton" {
resource "triton_machine" "test" {
name = "%s"
package = "g4-highcpu-128M"
image = "e1faace4-e19b-11e5-928b-83849e2fd94a"
image = "fb5fe970-e6e4-11e6-9820-4b51be190db9"
}
output "domain_names" {
value = "${join(", ", triton_machine.test.domain_names)}"
}

View File

@ -5,7 +5,7 @@ import (
"strconv"
"github.com/hashicorp/terraform/helper/schema"
"github.com/joyent/gosdc/cloudapi"
"github.com/joyent/triton-go"
)
func resourceVLAN() *schema.Resource {
@ -15,13 +15,14 @@ func resourceVLAN() *schema.Resource {
Read: resourceVLANRead,
Update: resourceVLANUpdate,
Delete: resourceVLANDelete,
Timeouts: fastResourceTimeout,
Importer: &schema.ResourceImporter{
State: resourceVLANImporter,
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"vlan_id": {
Description: "number between 0-4095 indicating VLAN ID",
Description: "Number between 0-4095 indicating VLAN ID",
Required: true,
ForceNew: true,
Type: schema.TypeInt,
@ -39,7 +40,7 @@ func resourceVLAN() *schema.Resource {
Type: schema.TypeString,
},
"description": {
Description: "Optional description of the VLAN",
Description: "Description of the VLAN",
Optional: true,
Type: schema.TypeString,
},
@ -48,10 +49,10 @@ func resourceVLAN() *schema.Resource {
}
func resourceVLANCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
vlan, err := client.CreateFabricVLAN(cloudapi.FabricVLAN{
Id: int16(d.Get("vlan_id").(int)),
vlan, err := client.Fabrics().CreateFabricVLAN(&triton.CreateFabricVLANInput{
ID: d.Get("vlan_id").(int),
Name: d.Get("name").(string),
Description: d.Get("description").(string),
})
@ -59,33 +60,39 @@ func resourceVLANCreate(d *schema.ResourceData, meta interface{}) error {
return err
}
d.SetId(resourceVLANIDString(vlan.Id))
d.SetId(strconv.Itoa(vlan.ID))
return resourceVLANRead(d, meta)
}
func resourceVLANExists(d *schema.ResourceData, meta interface{}) (bool, error) {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
id, err := resourceVLANIDInt16(d.Id())
id, err := resourceVLANIDInt(d.Id())
if err != nil {
return false, err
}
vlan, err := client.GetFabricVLAN(id)
return vlan != nil && err == nil, err
return resourceExists(client.Fabrics().GetFabricVLAN(&triton.GetFabricVLANInput{
ID: id,
}))
}
func resourceVLANRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
vlan, err := client.GetFabricVLAN(int16(d.Get("vlan_id").(int)))
id, err := resourceVLANIDInt(d.Id())
if err != nil {
return err
}
d.SetId(resourceVLANIDString(vlan.Id))
d.Set("vlan_id", vlan.Id)
vlan, err := client.Fabrics().GetFabricVLAN(&triton.GetFabricVLANInput{
ID: id,
})
if err != nil {
return err
}
d.Set("vlan_id", vlan.ID)
d.Set("name", vlan.Name)
d.Set("description", vlan.Description)
@ -93,10 +100,10 @@ func resourceVLANRead(d *schema.ResourceData, meta interface{}) error {
}
func resourceVLANUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
vlan, err := client.UpdateFabricVLAN(cloudapi.FabricVLAN{
Id: int16(d.Get("vlan_id").(int)),
vlan, err := client.Fabrics().UpdateFabricVLAN(&triton.UpdateFabricVLANInput{
ID: d.Get("vlan_id").(int),
Name: d.Get("name").(string),
Description: d.Get("description").(string),
})
@ -104,36 +111,28 @@ func resourceVLANUpdate(d *schema.ResourceData, meta interface{}) error {
return err
}
d.SetId(resourceVLANIDString(vlan.Id))
d.SetId(strconv.Itoa(vlan.ID))
return resourceVLANRead(d, meta)
}
func resourceVLANDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudapi.Client)
client := meta.(*triton.Client)
id, err := resourceVLANIDInt16(d.Id())
id, err := resourceVLANIDInt(d.Id())
if err != nil {
return err
}
return client.DeleteFabricVLAN(id)
return client.Fabrics().DeleteFabricVLAN(&triton.DeleteFabricVLANInput{
ID: id,
})
}
// convenience conversion functions
func resourceVLANIDString(id int16) string {
return strconv.Itoa(int(id))
}
func resourceVLANIDInt16(id string) (int16, error) {
result, err := strconv.ParseInt(id, 10, 16)
func resourceVLANIDInt(id string) (int, error) {
result, err := strconv.ParseInt(id, 10, 32)
if err != nil {
return 0, err
return -1, err
}
return int16(result), nil
}
func resourceVLANImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
return []*schema.ResourceData{d}, nil
return int(result), nil
}

View File

@ -2,22 +2,24 @@ package triton
import (
"fmt"
"strconv"
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/joyent/gosdc/cloudapi"
"github.com/joyent/triton-go"
)
func TestAccTritonVLAN_basic(t *testing.T) {
config := testAccTritonVLAN_basic
config := testAccTritonVLAN_basic(acctest.RandIntRange(3, 2048))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckTritonVLANDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
testCheckTritonVLANExists("triton_vlan.test"),
@ -28,27 +30,30 @@ func TestAccTritonVLAN_basic(t *testing.T) {
}
func TestAccTritonVLAN_update(t *testing.T) {
preConfig := testAccTritonVLAN_basic
postConfig := testAccTritonVLAN_update
vlanNumber := acctest.RandIntRange(3, 2048)
preConfig := testAccTritonVLAN_basic(vlanNumber)
postConfig := testAccTritonVLAN_update(vlanNumber)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckTritonVLANDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: preConfig,
Check: resource.ComposeTestCheckFunc(
testCheckTritonVLANExists("triton_vlan.test"),
resource.TestCheckResourceAttr("triton_vlan.test", "vlan_id", strconv.Itoa(vlanNumber)),
resource.TestCheckResourceAttr("triton_vlan.test", "name", "test-vlan"),
resource.TestCheckResourceAttr("triton_vlan.test", "description", "test vlan"),
),
},
resource.TestStep{
{
Config: postConfig,
Check: resource.ComposeTestCheckFunc(
testCheckTritonVLANExists("triton_vlan.test"),
resource.TestCheckResourceAttr("triton_vlan.test", "vlan_id", strconv.Itoa(vlanNumber)),
resource.TestCheckResourceAttr("triton_vlan.test", "name", "test-vlan-2"),
resource.TestCheckResourceAttr("triton_vlan.test", "description", "test vlan 2"),
),
@ -64,19 +69,23 @@ func testCheckTritonVLANExists(name string) resource.TestCheckFunc {
if !ok {
return fmt.Errorf("Not found: %s", name)
}
conn := testAccProvider.Meta().(*cloudapi.Client)
conn := testAccProvider.Meta().(*triton.Client)
id, err := resourceVLANIDInt16(rs.Primary.ID)
id, err := resourceVLANIDInt(rs.Primary.ID)
if err != nil {
return err
}
rule, err := conn.GetFabricVLAN(id)
if err != nil {
resp, err := conn.Fabrics().GetFabricVLAN(&triton.GetFabricVLANInput{
ID: id,
})
if err != nil && triton.IsResourceNotFound(err) {
return fmt.Errorf("Bad: Check VLAN Exists: %s", err)
} else if err != nil {
return err
}
if rule == nil {
if resp == nil {
return fmt.Errorf("Bad: VLAN %q does not exist", rs.Primary.ID)
}
@ -85,21 +94,25 @@ func testCheckTritonVLANExists(name string) resource.TestCheckFunc {
}
func testCheckTritonVLANDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*cloudapi.Client)
conn := testAccProvider.Meta().(*triton.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "triton_vlan" {
continue
}
id, err := resourceVLANIDInt16(rs.Primary.ID)
id, err := resourceVLANIDInt(rs.Primary.ID)
if err != nil {
return err
}
resp, err := conn.GetFabricVLAN(id)
if err != nil {
resp, err := conn.Fabrics().GetFabricVLAN(&triton.GetFabricVLANInput{
ID: id,
})
if triton.IsResourceNotFound(err) {
return nil
} else if err != nil {
return err
}
if resp != nil {
@ -110,18 +123,18 @@ func testCheckTritonVLANDestroy(s *terraform.State) error {
return nil
}
var testAccTritonVLAN_basic = `
resource "triton_vlan" "test" {
vlan_id = 1024
var testAccTritonVLAN_basic = func(vlanID int) string {
return fmt.Sprintf(`resource "triton_vlan" "test" {
vlan_id = %d
name = "test-vlan"
description = "test vlan"
}`, vlanID)
}
`
var testAccTritonVLAN_update = `
resource "triton_vlan" "test" {
vlan_id = 1024
var testAccTritonVLAN_update = func(vlanID int) string {
return fmt.Sprintf(`resource "triton_vlan" "test" {
vlan_id = %d
name = "test-vlan-2"
description = "test vlan 2"
}`, vlanID)
}
`

View File

@ -1,30 +0,0 @@
package triton
import (
"errors"
"time"
)
var (
// ErrTimeout is returned when waiting for state change
ErrTimeout = errors.New("timed out while waiting for resource change")
)
func waitFor(f func() (bool, error), every, timeout time.Duration) error {
start := time.Now()
for time.Since(start) <= timeout {
stop, err := f()
if err != nil {
return err
}
if stop {
return nil
}
time.Sleep(every)
}
return ErrTimeout
}

View File

@ -1,4 +1,4 @@
// Code generated by "stringer -type=countHookAction hook_count_action.go"; DO NOT EDIT
// Code generated by "stringer -type=countHookAction hook_count_action.go"; DO NOT EDIT.
package command

View File

@ -406,6 +406,134 @@ func TestInit_copyBackendDst(t *testing.T) {
}
}
func TestInit_backendReinitWithExtra(t *testing.T) {
td := tempDir(t)
copy.CopyDir(testFixturePath("init-backend-empty"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
m := testMetaBackend(t, nil)
opts := &BackendOpts{
ConfigExtra: map[string]interface{}{"path": "hello"},
Init: true,
}
b, err := m.backendConfig(opts)
if err != nil {
t.Fatal(err)
}
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{"-backend-config", "path=hello"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
// Read our saved backend config and verify we have our settings
state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename))
if v := state.Backend.Config["path"]; v != "hello" {
t.Fatalf("bad: %#v", v)
}
if state.Backend.Hash != b.Hash {
t.Fatal("mismatched state and config backend hashes")
}
if state.Backend.Rehash() != b.Rehash() {
t.Fatal("mismatched state and config re-hashes")
}
// init again and make sure nothing changes
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
state = testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename))
if v := state.Backend.Config["path"]; v != "hello" {
t.Fatalf("bad: %#v", v)
}
if state.Backend.Hash != b.Hash {
t.Fatal("mismatched state and config backend hashes")
}
}
// move option from config to -backend-config args
func TestInit_backendReinitConfigToExtra(t *testing.T) {
td := tempDir(t)
copy.CopyDir(testFixturePath("init-backend"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
if code := c.Run([]string{"-input=false"}); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
// Read our saved backend config and verify we have our settings
state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename))
if v := state.Backend.Config["path"]; v != "foo" {
t.Fatalf("bad: %#v", v)
}
backendHash := state.Backend.Hash
// init again but remove the path option from the config
cfg := "terraform {\n backend \"local\" {}\n}\n"
if err := ioutil.WriteFile("main.tf", []byte(cfg), 0644); err != nil {
t.Fatal(err)
}
args := []string{"-input=false", "-backend-config=path=foo"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
state = testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename))
if state.Backend.Hash == backendHash {
t.Fatal("state.Backend.Hash was not updated")
}
}
// make sure inputFalse stops execution on migrate
func TestInit_inputFalse(t *testing.T) {
td := tempDir(t)
copy.CopyDir(testFixturePath("init-backend"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{"-input=false", "-backend-config=path=foo"}
if code := c.Run([]string{"-input=false"}); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter)
}
args = []string{"-input=false", "-backend-config=path=bar"}
if code := c.Run(args); code == 0 {
t.Fatal("init should have failed", ui.OutputWriter)
}
}
/*
func TestInit_remoteState(t *testing.T) {
tmp, cwd := testCwd(t)

View File

@ -3,6 +3,7 @@ package command
import (
"bufio"
"bytes"
"errors"
"flag"
"fmt"
"io"
@ -341,6 +342,9 @@ func (m *Meta) uiHook() *UiHook {
// confirm asks a yes/no confirmation.
func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) {
if !m.input {
return false, errors.New("input disabled")
}
for {
v, err := m.UIInput().Input(opts)
if err != nil {

View File

@ -415,9 +415,17 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) {
case c != nil && s.Remote.Empty() && !s.Backend.Empty():
// If our configuration is the same, then we're just initializing
// a previously configured remote backend.
if !s.Backend.Empty() && s.Backend.Hash == cHash {
if !s.Backend.Empty() {
hash := s.Backend.Hash
// on init we need an updated hash containing any extra options
// that were added after merging.
if opts.Init {
hash = s.Backend.Rehash()
}
if hash == cHash {
return m.backend_C_r_S_unchanged(c, sMgr)
}
}
if !opts.Init {
initReason := fmt.Sprintf(
@ -451,7 +459,11 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) {
case c != nil && !s.Remote.Empty() && !s.Backend.Empty():
// If the hashes are the same, we have a legacy remote state with
// an unchanged stored backend state.
if s.Backend.Hash == cHash {
hash := s.Backend.Hash
if opts.Init {
hash = s.Backend.Rehash()
}
if hash == cHash {
if !opts.Init {
initReason := fmt.Sprintf(
"Legacy remote state found with configured backend %q",
@ -1146,6 +1158,16 @@ func (m *Meta) backend_C_r_S_unchanged(
c *config.Backend, sMgr state.State) (backend.Backend, error) {
s := sMgr.State()
// it's possible for a backend to be unchanged, and the config itself to
// have changed by moving a paramter from the config to `-backend-config`
// In this case we only need to update the Hash.
if c != nil && s.Backend.Hash != c.Hash {
s.Backend.Hash = c.Hash
if err := sMgr.WriteState(s); err != nil {
return nil, fmt.Errorf(errBackendWriteSaved, err)
}
}
// Create the config. We do this from the backend state since this
// has the complete configuration data whereas the config itself
// may require input.

View File

@ -3217,6 +3217,110 @@ func TestMetaBackend_planLegacy(t *testing.T) {
}
}
// init a backend using -backend-config options multiple times
func TestMetaBackend_configureWithExtra(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("init-backend-empty"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
extras := map[string]interface{}{"path": "hello"}
m := testMetaBackend(t, nil)
opts := &BackendOpts{
ConfigExtra: extras,
Init: true,
}
backendCfg, err := m.backendConfig(opts)
if err != nil {
t.Fatal(err)
}
// init the backend
_, err = m.Backend(&BackendOpts{
ConfigExtra: extras,
Init: true,
})
if err != nil {
t.Fatalf("bad: %s", err)
}
// Check the state
s := testStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename))
if s.Backend.Hash != backendCfg.Hash {
t.Fatal("mismatched state and config backend hashes")
}
if s.Backend.Rehash() == s.Backend.Hash {
t.Fatal("saved hash should not match actual hash")
}
if s.Backend.Rehash() != backendCfg.Rehash() {
t.Fatal("mismatched state and config re-hashes")
}
// init the backend again with the same options
m = testMetaBackend(t, nil)
_, err = m.Backend(&BackendOpts{
ConfigExtra: extras,
Init: true,
})
if err != nil {
t.Fatalf("bad: %s", err)
}
// Check the state
s = testStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename))
if s.Backend.Hash != backendCfg.Hash {
t.Fatal("mismatched state and config backend hashes")
}
}
// move options from config to -backend-config
func TestMetaBackend_configToExtra(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("init-backend"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// init the backend
m := testMetaBackend(t, nil)
_, err := m.Backend(&BackendOpts{
Init: true,
})
if err != nil {
t.Fatalf("bad: %s", err)
}
// Check the state
s := testStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename))
backendHash := s.Backend.Hash
// init again but remove the path option from the config
cfg := "terraform {\n backend \"local\" {}\n}\n"
if err := ioutil.WriteFile("main.tf", []byte(cfg), 0644); err != nil {
t.Fatal(err)
}
// init the backend again with the options
extras := map[string]interface{}{"path": "hello"}
m = testMetaBackend(t, nil)
m.forceInitCopy = true
_, err = m.Backend(&BackendOpts{
ConfigExtra: extras,
Init: true,
})
if err != nil {
t.Fatalf("bad: %s", err)
}
s = testStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename))
if s.Backend.Hash == backendHash {
t.Fatal("state.Backend.Hash was not updated")
}
}
func testMetaBackend(t *testing.T, args []string) *Meta {
var m Meta
m.Ui = new(cli.MockUi)

View File

@ -0,0 +1,4 @@
terraform {
backend "local" {
}
}

View File

@ -1,3 +1,8 @@
variable "var_with_escaped_interp" {
# This is here because in the past it failed. See Github #13001
default = "foo-$${bar.baz}"
}
resource "test_instance" "foo" {
ami = "bar"

View File

@ -285,8 +285,15 @@ func (c *Config) Validate() error {
}
interp := false
fn := func(ast.Node) (interface{}, error) {
fn := func(n ast.Node) (interface{}, error) {
// LiteralNode is a literal string (outside of a ${ ... } sequence).
// interpolationWalker skips most of these. but in particular it
// visits those that have escaped sequences (like $${foo}) as a
// signal that *some* processing is required on this string. For
// our purposes here though, this is fine and not an interpolation.
if _, ok := n.(*ast.LiteralNode); !ok {
interp = true
}
return "", nil
}

View File

@ -72,7 +72,7 @@ type Backend struct {
Hash uint64
}
// Hash returns a unique content hash for this backend's configuration
// Rehash returns a unique content hash for this backend's configuration
// as a uint64 value.
func (b *Backend) Rehash() uint64 {
// If we have no backend, the value is zero

Some files were not shown because too many files have changed in this diff Show More