diff --git a/CHANGELOG.md b/CHANGELOG.md index 32c5b6359..7b70bdf62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ ## 0.6.0 (Unreleased) +FEATURES: + + * **New provider: `azure`** [GH-2053] + * **New resource: `aws_autoscaling_notification`** [GH-2197] + * **New resource: `aws_ecs_cluster`** [GH-1803] + * **New resource: `aws_ecs_service`** [GH-1803] + * **New resource: `aws_ecs_task_definition`** [GH-1803] + * **New remote state backend: `swift`**: You can now store remote state in + a OpenStack Swift. [GH-2254] + * command/output: support display of module outputs [GH-2102] + * core: keys() and values() funcs for map variables [GH-2198] + +IMPROVEMENTS: + + * core: HTTP remote state now accepts `skip_cert_verification` + option to ignore TLS cert verification. [GH-2214] + * provider/aws: ElastiCache Subnet Groups can be updated + without destroying first [GH-2191] + * provider/docker: `docker_container` has the `privileged` + option. [GH-2227] + * provider/openstack: allow `OS_AUTH_TOKEN` environment variable + to set the openstack `api_key` field [GH-2234] + * provider/openstack: Can now configure endpoint type (public, admin, + internal) [GH-2262] + +BUG FIXES: + + * provider/aws: fix panic when route has no cidr_block [GH-2215] + * provider/aws: fix issue causing perpetual diff on ELB listeners + when non-lowercase protocol strings were used [GH-2246] + * provider/aws: corrected frankfurt S3 website region [GH-2259] + * provider/aws: `aws_elasticache_cluster` port is required [GH-2160] + ## 0.5.3 (June 1, 2015) IMPROVEMENTS: @@ -140,7 +173,7 @@ IMPROVEMENTS: * **New resource: `google_dns_record_set`** * **Migrate to upstream AWS SDK:** Migrate the AWS provider to [awslabs/aws-sdk-go](https://github.com/awslabs/aws-sdk-go), - the offical `awslabs` library. Previously we had forked the library for + the official `awslabs` library. Previously we had forked the library for stability while `awslabs` refactored. Now that work has completed, and we've migrated back to the upstream version. * core: Improve error message on diff mismatch [GH-1501] @@ -168,7 +201,7 @@ IMPROVEMENTS: * provider/aws: `aws_network_acl` improved validation for network ACL ports and protocols [GH-1798] [GH-1808] * provider/aws: `aws_route_table` can target network interfaces [GH-968] - * provider/aws: `aws_route_table` can specify propogating VGWs [GH-1516] + * provider/aws: `aws_route_table` can specify propagating VGWs [GH-1516] * provider/aws: `aws_route53_record` supports weighted sets [GH-1578] * provider/aws: `aws_route53_zone` exports nameservers [GH-1525] * provider/aws: `aws_s3_bucket` website support [GH-1738] @@ -325,7 +358,7 @@ FEATURES: * **Math operations** in interpolations. You can now do things like `${count.index+1}`. [GH-1068] * **New AWS SDK:** Move to `aws-sdk-go` (hashicorp/aws-sdk-go), - a fork of the offical `awslabs` repo. We forked for stability while + a fork of the official `awslabs` repo. We forked for stability while `awslabs` refactored the library, and will move back to the officially supported version in the next release. @@ -354,7 +387,7 @@ IMPROVEMENTS: * providers/aws: Improve dependency violation error handling, when deleting Internet Gateways or Auto Scaling groups [GH-1325]. * provider/aws: Add non-destructive updates to AWS RDS. You can now upgrade - `egine_version`, `parameter_group_name`, and `multi_az` without forcing + `engine_version`, `parameter_group_name`, and `multi_az` without forcing a new database to be created.[GH-1341] * providers/aws: Full support for block device mappings on instances and launch configurations [GH-1045, GH-1364] diff --git a/builtin/bins/provider-azure/main.go b/builtin/bins/provider-azure/main.go new file mode 100644 index 000000000..45af21656 --- /dev/null +++ b/builtin/bins/provider-azure/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/azure" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: azure.Provider, + }) +} diff --git a/builtin/bins/provider-azure/main_test.go b/builtin/bins/provider-azure/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provider-azure/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/aws/autoscaling_tags.go b/builtin/providers/aws/autoscaling_tags.go index ee4724852..9a41a3291 100644 --- a/builtin/providers/aws/autoscaling_tags.go +++ b/builtin/providers/aws/autoscaling_tags.go @@ -5,8 +5,8 @@ import ( "fmt" "log" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/autoscaling" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/autoscaling_tags_test.go b/builtin/providers/aws/autoscaling_tags_test.go index c14e366ce..b401fa2e4 100644 --- a/builtin/providers/aws/autoscaling_tags_test.go +++ b/builtin/providers/aws/autoscaling_tags_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/awslabs/aws-sdk-go/service/autoscaling" + "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 9d8397aef..6a46b5fc9 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -7,19 +7,20 @@ import ( "github.com/hashicorp/terraform/helper/multierror" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/credentials" - "github.com/awslabs/aws-sdk-go/service/autoscaling" - "github.com/awslabs/aws-sdk-go/service/ec2" - "github.com/awslabs/aws-sdk-go/service/elasticache" - "github.com/awslabs/aws-sdk-go/service/elb" - "github.com/awslabs/aws-sdk-go/service/iam" - "github.com/awslabs/aws-sdk-go/service/kinesis" - "github.com/awslabs/aws-sdk-go/service/rds" - "github.com/awslabs/aws-sdk-go/service/route53" - "github.com/awslabs/aws-sdk-go/service/s3" - "github.com/awslabs/aws-sdk-go/service/sns" - "github.com/awslabs/aws-sdk-go/service/sqs" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/aws/aws-sdk-go/service/elb" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/kinesis" + "github.com/aws/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/sns" + "github.com/aws/aws-sdk-go/service/sqs" ) type Config struct { @@ -35,6 +36,7 @@ type Config struct { type AWSClient struct { ec2conn *ec2.EC2 + ecsconn *ecs.ECS elbconn *elb.ELB autoscalingconn *autoscaling.AutoScaling s3conn *s3.S3 @@ -116,6 +118,9 @@ func (c *Config) Client() (interface{}, error) { log.Println("[INFO] Initializing EC2 Connection") client.ec2conn = ec2.New(awsConfig) + log.Println("[INFO] Initializing ECS Connection") + client.ecsconn = ecs.New(awsConfig) + // aws-sdk-go uses v4 for signing requests, which requires all global // endpoints to use 'us-east-1'. // See http://docs.aws.amazon.com/general/latest/gr/sigv4_changes.html diff --git a/builtin/providers/aws/network_acl_entry.go b/builtin/providers/aws/network_acl_entry.go index e328d1ae4..4d5b2c22c 100644 --- a/builtin/providers/aws/network_acl_entry.go +++ b/builtin/providers/aws/network_acl_entry.go @@ -5,8 +5,8 @@ import ( "net" "strconv" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" ) func expandNetworkAclEntries(configured []interface{}, entryType string) ([]*ec2.NetworkACLEntry, error) { diff --git a/builtin/providers/aws/network_acl_entry_test.go b/builtin/providers/aws/network_acl_entry_test.go index 95fa69dae..c34b7dfc3 100644 --- a/builtin/providers/aws/network_acl_entry_test.go +++ b/builtin/providers/aws/network_acl_entry_test.go @@ -4,8 +4,8 @@ import ( "reflect" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" ) func Test_expandNetworkACLEntry(t *testing.T) { diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 9e0f928a4..de2d9becc 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -85,12 +85,16 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(), "aws_autoscaling_group": resourceAwsAutoscalingGroup(), + "aws_autoscaling_notification": resourceAwsAutoscalingNotification(), "aws_customer_gateway": resourceAwsCustomerGateway(), "aws_db_instance": resourceAwsDbInstance(), "aws_db_parameter_group": resourceAwsDbParameterGroup(), "aws_db_security_group": resourceAwsDbSecurityGroup(), "aws_db_subnet_group": resourceAwsDbSubnetGroup(), "aws_ebs_volume": resourceAwsEbsVolume(), + "aws_ecs_cluster": resourceAwsEcsCluster(), + "aws_ecs_service": resourceAwsEcsService(), + "aws_ecs_task_definition": resourceAwsEcsTaskDefinition(), "aws_eip": resourceAwsEip(), "aws_elasticache_cluster": resourceAwsElasticacheCluster(), "aws_elasticache_security_group": resourceAwsElasticacheSecurityGroup(), diff --git a/builtin/providers/aws/resource_aws_app_cookie_stickiness_policy.go b/builtin/providers/aws/resource_aws_app_cookie_stickiness_policy.go index 66c240826..ca459ead7 100644 --- a/builtin/providers/aws/resource_aws_app_cookie_stickiness_policy.go +++ b/builtin/providers/aws/resource_aws_app_cookie_stickiness_policy.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/elb" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/elb" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/resource_aws_app_cookie_stickiness_policy_test.go b/builtin/providers/aws/resource_aws_app_cookie_stickiness_policy_test.go index 3e99ab71d..ff13da285 100644 --- a/builtin/providers/aws/resource_aws_app_cookie_stickiness_policy_test.go +++ b/builtin/providers/aws/resource_aws_app_cookie_stickiness_policy_test.go @@ -4,14 +4,14 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/elb" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elb" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) -func TestAccAWSAppCookieStickinessPolicy(t *testing.T) { +func TestAccAWSAppCookieStickinessPolicy_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, diff --git a/builtin/providers/aws/resource_aws_autoscaling_group.go b/builtin/providers/aws/resource_aws_autoscaling_group.go index 60f3cba85..0fc62b4b2 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group.go @@ -9,10 +9,10 @@ import ( "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/autoscaling" - "github.com/awslabs/aws-sdk-go/service/elb" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/aws/aws-sdk-go/service/elb" ) func resourceAwsAutoscalingGroup() *schema.Resource { @@ -462,7 +462,7 @@ func waitForASGCapacity(d *schema.ResourceData, meta interface{}) error { return nil } - return fmt.Errorf("Still need to wait for more healthy instances.") + return fmt.Errorf("Still need to wait for more healthy instances. This could mean instances failed to launch. See Scaling History for more information.") }) } diff --git a/builtin/providers/aws/resource_aws_autoscaling_group_test.go b/builtin/providers/aws/resource_aws_autoscaling_group_test.go index 49897c4f0..cc3cdf6e5 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group_test.go @@ -6,9 +6,9 @@ import ( "strings" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/autoscaling" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) diff --git a/builtin/providers/aws/resource_aws_autoscaling_notification.go b/builtin/providers/aws/resource_aws_autoscaling_notification.go new file mode 100644 index 000000000..07adaa676 --- /dev/null +++ b/builtin/providers/aws/resource_aws_autoscaling_notification.go @@ -0,0 +1,200 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsAutoscalingNotification() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAutoscalingNotificationCreate, + Read: resourceAwsAutoscalingNotificationRead, + Update: resourceAwsAutoscalingNotificationUpdate, + Delete: resourceAwsAutoscalingNotificationDelete, + + Schema: map[string]*schema.Schema{ + "topic_arn": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "group_names": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "notifications": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + } +} + +func resourceAwsAutoscalingNotificationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + gl := convertSetToList(d.Get("group_names").(*schema.Set)) + nl := convertSetToList(d.Get("notifications").(*schema.Set)) + + topic := d.Get("topic_arn").(string) + if err := addNotificationConfigToGroupsWithTopic(conn, gl, nl, topic); err != nil { + return err + } + + // ARNs are unique, and these notifications are per ARN, so we re-use the ARN + // here as the ID + d.SetId(topic) + return resourceAwsAutoscalingNotificationRead(d, meta) +} + +func resourceAwsAutoscalingNotificationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + gl := convertSetToList(d.Get("group_names").(*schema.Set)) + + opts := &autoscaling.DescribeNotificationConfigurationsInput{ + AutoScalingGroupNames: gl, + } + + resp, err := conn.DescribeNotificationConfigurations(opts) + if err != nil { + return fmt.Errorf("Error describing notifications") + } + + topic := d.Get("topic_arn").(string) + // Grab all applicable notifcation configurations for this Topic. + // Each NotificationType will have a record, so 1 Group with 3 Types results + // in 3 records, all with the same Group name + gRaw := make(map[string]bool) + nRaw := make(map[string]bool) + for _, n := range resp.NotificationConfigurations { + if *n.TopicARN == topic { + gRaw[*n.AutoScalingGroupName] = true + nRaw[*n.NotificationType] = true + } + } + + // Grab the keys here as the list of Groups + var gList []string + for k, _ := range gRaw { + gList = append(gList, k) + } + + // Grab the keys here as the list of Types + var nList []string + for k, _ := range nRaw { + nList = append(nList, k) + } + + if err := d.Set("group_names", gList); err != nil { + return err + } + if err := d.Set("notifications", nList); err != nil { + return err + } + + return nil +} + +func resourceAwsAutoscalingNotificationUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + + // Notifications API call is a PUT, so we don't need to diff the list, just + // push whatever it is and AWS sorts it out + nl := convertSetToList(d.Get("notifications").(*schema.Set)) + + o, n := d.GetChange("group_names") + if o == nil { + o = new(schema.Set) + } + if n == nil { + n = new(schema.Set) + } + + os := o.(*schema.Set) + ns := n.(*schema.Set) + remove := convertSetToList(os.Difference(ns)) + add := convertSetToList(ns.Difference(os)) + + topic := d.Get("topic_arn").(string) + + if err := removeNotificationConfigToGroupsWithTopic(conn, remove, topic); err != nil { + return err + } + + var update []*string + if d.HasChange("notifications") { + update = convertSetToList(d.Get("group_names").(*schema.Set)) + } else { + update = add + } + + if err := addNotificationConfigToGroupsWithTopic(conn, update, nl, topic); err != nil { + return err + } + + return resourceAwsAutoscalingNotificationRead(d, meta) +} + +func addNotificationConfigToGroupsWithTopic(conn *autoscaling.AutoScaling, groups []*string, nl []*string, topic string) error { + for _, a := range groups { + opts := &autoscaling.PutNotificationConfigurationInput{ + AutoScalingGroupName: a, + NotificationTypes: nl, + TopicARN: aws.String(topic), + } + + _, err := conn.PutNotificationConfiguration(opts) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + return fmt.Errorf("[WARN] Error creating Autoscaling Group Notification for Group %s, error: \"%s\", code: \"%s\"", *a, awsErr.Message(), awsErr.Code()) + } + return err + } + } + return nil +} + +func removeNotificationConfigToGroupsWithTopic(conn *autoscaling.AutoScaling, groups []*string, topic string) error { + for _, r := range groups { + opts := &autoscaling.DeleteNotificationConfigurationInput{ + AutoScalingGroupName: r, + TopicARN: aws.String(topic), + } + + _, err := conn.DeleteNotificationConfiguration(opts) + if err != nil { + return fmt.Errorf("[WARN] Error deleting notification configuration for ASG \"%s\", Topic ARN \"%s\"", *r, topic) + } + } + return nil +} + +func resourceAwsAutoscalingNotificationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + gl := convertSetToList(d.Get("group_names").(*schema.Set)) + + topic := d.Get("topic_arn").(string) + if err := removeNotificationConfigToGroupsWithTopic(conn, gl, topic); err != nil { + return err + } + + return nil +} + +func convertSetToList(s *schema.Set) (nl []*string) { + l := s.List() + for _, n := range l { + nl = append(nl, aws.String(n.(string))) + } + + return nl +} diff --git a/builtin/providers/aws/resource_aws_autoscaling_notification_test.go b/builtin/providers/aws/resource_aws_autoscaling_notification_test.go new file mode 100644 index 000000000..a9b078a6e --- /dev/null +++ b/builtin/providers/aws/resource_aws_autoscaling_notification_test.go @@ -0,0 +1,252 @@ +package aws + +import ( + "fmt" + "strconv" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccASGNotification_basic(t *testing.T) { + var asgn autoscaling.DescribeNotificationConfigurationsOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckASGNDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccASGNotificationConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckASGNotificationExists("aws_autoscaling_notification.example", []string{"foobar1-terraform-test"}, &asgn), + testAccCheckAWSASGNotificationAttributes("aws_autoscaling_notification.example", &asgn), + ), + }, + }, + }) +} + +func TestAccASGNotification_update(t *testing.T) { + var asgn autoscaling.DescribeNotificationConfigurationsOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckASGNDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccASGNotificationConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckASGNotificationExists("aws_autoscaling_notification.example", []string{"foobar1-terraform-test"}, &asgn), + testAccCheckAWSASGNotificationAttributes("aws_autoscaling_notification.example", &asgn), + ), + }, + + resource.TestStep{ + Config: testAccASGNotificationConfig_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckASGNotificationExists("aws_autoscaling_notification.example", []string{"foobar1-terraform-test", "barfoo-terraform-test"}, &asgn), + testAccCheckAWSASGNotificationAttributes("aws_autoscaling_notification.example", &asgn), + ), + }, + }, + }) +} + +func testAccCheckASGNotificationExists(n string, groups []string, asgn *autoscaling.DescribeNotificationConfigurationsOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ASG Notification ID is set") + } + + var gl []*string + for _, g := range groups { + gl = append(gl, aws.String(g)) + } + + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + opts := &autoscaling.DescribeNotificationConfigurationsInput{ + AutoScalingGroupNames: gl, + } + + resp, err := conn.DescribeNotificationConfigurations(opts) + if err != nil { + return fmt.Errorf("Error describing notifications") + } + + *asgn = *resp + + return nil + } +} + +func testAccCheckASGNDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_autoscaling_notification" { + continue + } + + groups := []*string{aws.String("foobar1-terraform-test")} + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + opts := &autoscaling.DescribeNotificationConfigurationsInput{ + AutoScalingGroupNames: groups, + } + + resp, err := conn.DescribeNotificationConfigurations(opts) + if err != nil { + return fmt.Errorf("Error describing notifications") + } + + if len(resp.NotificationConfigurations) != 0 { + fmt.Errorf("Error finding notification descriptions") + } + + } + return nil +} + +func testAccCheckAWSASGNotificationAttributes(n string, asgn *autoscaling.DescribeNotificationConfigurationsOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ASG Notification ID is set") + } + + if len(asgn.NotificationConfigurations) == 0 { + return fmt.Errorf("Error: no ASG Notifications found") + } + + // build a unique list of groups, notification types + gRaw := make(map[string]bool) + nRaw := make(map[string]bool) + for _, n := range asgn.NotificationConfigurations { + if *n.TopicARN == rs.Primary.Attributes["topic_arn"] { + gRaw[*n.AutoScalingGroupName] = true + nRaw[*n.NotificationType] = true + } + } + + // Grab the keys here as the list of Groups + var gList []string + for k, _ := range gRaw { + gList = append(gList, k) + } + + // Grab the keys here as the list of Types + var nList []string + for k, _ := range nRaw { + nList = append(nList, k) + } + + typeCount, _ := strconv.Atoi(rs.Primary.Attributes["notifications.#"]) + + if len(nList) != typeCount { + return fmt.Errorf("Error: Bad ASG Notification count, expected (%d), got (%d)", typeCount, len(nList)) + } + + groupCount, _ := strconv.Atoi(rs.Primary.Attributes["group_names.#"]) + + if len(gList) != groupCount { + return fmt.Errorf("Error: Bad ASG Group count, expected (%d), got (%d)", typeCount, len(gList)) + } + + return nil + } +} + +const testAccASGNotificationConfig_basic = ` +resource "aws_sns_topic" "topic_example" { + name = "user-updates-topic" +} + +resource "aws_launch_configuration" "foobar" { + name = "foobarautoscaling-terraform-test" + image_id = "ami-21f78e11" + instance_type = "t1.micro" +} + +resource "aws_autoscaling_group" "bar" { + availability_zones = ["us-west-2a"] + name = "foobar1-terraform-test" + max_size = 1 + min_size = 1 + health_check_grace_period = 100 + health_check_type = "ELB" + desired_capacity = 1 + force_delete = true + termination_policies = ["OldestInstance"] + launch_configuration = "${aws_launch_configuration.foobar.name}" +} + +resource "aws_autoscaling_notification" "example" { + group_names = ["${aws_autoscaling_group.bar.name}"] + notifications = [ + "autoscaling:EC2_INSTANCE_LAUNCH", + "autoscaling:EC2_INSTANCE_TERMINATE", + ] + topic_arn = "${aws_sns_topic.topic_example.arn}" +} +` + +const testAccASGNotificationConfig_update = ` +resource "aws_sns_topic" "user_updates" { + name = "user-updates-topic" +} + +resource "aws_launch_configuration" "foobar" { + name = "foobarautoscaling-terraform-test" + image_id = "ami-21f78e11" + instance_type = "t1.micro" +} + +resource "aws_autoscaling_group" "bar" { + availability_zones = ["us-west-2a"] + name = "foobar1-terraform-test" + max_size = 1 + min_size = 1 + health_check_grace_period = 100 + health_check_type = "ELB" + desired_capacity = 1 + force_delete = true + termination_policies = ["OldestInstance"] + launch_configuration = "${aws_launch_configuration.foobar.name}" +} + +resource "aws_autoscaling_group" "foo" { + availability_zones = ["us-west-2b"] + name = "barfoo-terraform-test" + max_size = 1 + min_size = 1 + health_check_grace_period = 200 + health_check_type = "ELB" + desired_capacity = 1 + force_delete = true + termination_policies = ["OldestInstance"] + launch_configuration = "${aws_launch_configuration.foobar.name}" +} + +resource "aws_autoscaling_notification" "example" { + group_names = [ + "${aws_autoscaling_group.bar.name}", + "${aws_autoscaling_group.foo.name}", + ] + notifications = [ + "autoscaling:EC2_INSTANCE_LAUNCH", + "autoscaling:EC2_INSTANCE_TERMINATE", + "autoscaling:EC2_INSTANCE_LAUNCH_ERROR" + ] + topic_arn = "${aws_sns_topic.user_updates.arn}" +}` diff --git a/builtin/providers/aws/resource_aws_customer_gateway.go b/builtin/providers/aws/resource_aws_customer_gateway.go index 6fa1d39ab..fa4322304 100644 --- a/builtin/providers/aws/resource_aws_customer_gateway.go +++ b/builtin/providers/aws/resource_aws_customer_gateway.go @@ -5,9 +5,9 @@ import ( "log" "time" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" diff --git a/builtin/providers/aws/resource_aws_customer_gateway_test.go b/builtin/providers/aws/resource_aws_customer_gateway_test.go index 2a51eb112..33e370946 100644 --- a/builtin/providers/aws/resource_aws_customer_gateway_test.go +++ b/builtin/providers/aws/resource_aws_customer_gateway_test.go @@ -4,14 +4,14 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) -func TestAccCustomerGateway(t *testing.T) { +func TestAccCustomerGateway_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go index c832c67bb..705b773f9 100644 --- a/builtin/providers/aws/resource_aws_db_instance.go +++ b/builtin/providers/aws/resource_aws_db_instance.go @@ -6,10 +6,10 @@ import ( "strings" "time" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/iam" - "github.com/awslabs/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" diff --git a/builtin/providers/aws/resource_aws_db_instance_test.go b/builtin/providers/aws/resource_aws_db_instance_test.go index b6d31f75e..ef1931b03 100644 --- a/builtin/providers/aws/resource_aws_db_instance_test.go +++ b/builtin/providers/aws/resource_aws_db_instance_test.go @@ -9,12 +9,12 @@ import ( "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/rds" ) -func TestAccAWSDBInstance(t *testing.T) { +func TestAccAWSDBInstance_basic(t *testing.T) { var v rds.DBInstance resource.Test(t, resource.TestCase{ diff --git a/builtin/providers/aws/resource_aws_db_parameter_group.go b/builtin/providers/aws/resource_aws_db_parameter_group.go index 57758ae5b..9489394dc 100644 --- a/builtin/providers/aws/resource_aws_db_parameter_group.go +++ b/builtin/providers/aws/resource_aws_db_parameter_group.go @@ -11,9 +11,9 @@ import ( "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/rds" ) func resourceAwsDbParameterGroup() *schema.Resource { diff --git a/builtin/providers/aws/resource_aws_db_parameter_group_test.go b/builtin/providers/aws/resource_aws_db_parameter_group_test.go index 57400fb0c..93e74bb74 100644 --- a/builtin/providers/aws/resource_aws_db_parameter_group_test.go +++ b/builtin/providers/aws/resource_aws_db_parameter_group_test.go @@ -4,14 +4,14 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) -func TestAccAWSDBParameterGroup(t *testing.T) { +func TestAccAWSDBParameterGroup_basic(t *testing.T) { var v rds.DBParameterGroup resource.Test(t, resource.TestCase{ diff --git a/builtin/providers/aws/resource_aws_db_security_group.go b/builtin/providers/aws/resource_aws_db_security_group.go index 94d87e370..be535713d 100644 --- a/builtin/providers/aws/resource_aws_db_security_group.go +++ b/builtin/providers/aws/resource_aws_db_security_group.go @@ -6,9 +6,9 @@ import ( "log" "time" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/multierror" "github.com/hashicorp/terraform/helper/resource" diff --git a/builtin/providers/aws/resource_aws_db_security_group_test.go b/builtin/providers/aws/resource_aws_db_security_group_test.go index a2065114a..bf1db6e37 100644 --- a/builtin/providers/aws/resource_aws_db_security_group_test.go +++ b/builtin/providers/aws/resource_aws_db_security_group_test.go @@ -4,14 +4,14 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) -func TestAccAWSDBSecurityGroup(t *testing.T) { +func TestAccAWSDBSecurityGroup_basic(t *testing.T) { var v rds.DBSecurityGroup resource.Test(t, resource.TestCase{ diff --git a/builtin/providers/aws/resource_aws_db_subnet_group.go b/builtin/providers/aws/resource_aws_db_subnet_group.go index 598d291c4..5d49e9151 100644 --- a/builtin/providers/aws/resource_aws_db_subnet_group.go +++ b/builtin/providers/aws/resource_aws_db_subnet_group.go @@ -6,9 +6,9 @@ import ( "strings" "time" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/resource_aws_db_subnet_group_test.go b/builtin/providers/aws/resource_aws_db_subnet_group_test.go index 06e9e92c4..5a07652fe 100644 --- a/builtin/providers/aws/resource_aws_db_subnet_group_test.go +++ b/builtin/providers/aws/resource_aws_db_subnet_group_test.go @@ -7,12 +7,12 @@ import ( "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/rds" ) -func TestAccAWSDBSubnetGroup(t *testing.T) { +func TestAccAWSDBSubnetGroup_basic(t *testing.T) { var v rds.DBSubnetGroup testCheck := func(*terraform.State) error { diff --git a/builtin/providers/aws/resource_aws_ebs_volume.go b/builtin/providers/aws/resource_aws_ebs_volume.go index 9382605f8..d225f26ce 100644 --- a/builtin/providers/aws/resource_aws_ebs_volume.go +++ b/builtin/providers/aws/resource_aws_ebs_volume.go @@ -5,9 +5,9 @@ import ( "log" "time" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" diff --git a/builtin/providers/aws/resource_aws_ebs_volume_test.go b/builtin/providers/aws/resource_aws_ebs_volume_test.go index 7fb669707..729fbc151 100644 --- a/builtin/providers/aws/resource_aws_ebs_volume_test.go +++ b/builtin/providers/aws/resource_aws_ebs_volume_test.go @@ -4,13 +4,13 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) -func TestAccAWSEBSVolume(t *testing.T) { +func TestAccAWSEBSVolume_basic(t *testing.T) { var v ec2.Volume resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, diff --git a/builtin/providers/aws/resource_aws_ecs_cluster.go b/builtin/providers/aws/resource_aws_ecs_cluster.go new file mode 100644 index 000000000..30871f3dc --- /dev/null +++ b/builtin/providers/aws/resource_aws_ecs_cluster.go @@ -0,0 +1,80 @@ +package aws + +import ( + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsEcsCluster() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsEcsClusterCreate, + Read: resourceAwsEcsClusterRead, + Delete: resourceAwsEcsClusterDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsEcsClusterCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ecsconn + + clusterName := d.Get("name").(string) + log.Printf("[DEBUG] Creating ECS cluster %s", clusterName) + + out, err := conn.CreateCluster(&ecs.CreateClusterInput{ + ClusterName: aws.String(clusterName), + }) + if err != nil { + return err + } + log.Printf("[DEBUG] ECS cluster %s created", *out.Cluster.ClusterARN) + + d.SetId(*out.Cluster.ClusterARN) + d.Set("name", *out.Cluster.ClusterName) + return nil +} + +func resourceAwsEcsClusterRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ecsconn + + clusterName := d.Get("name").(string) + log.Printf("[DEBUG] Reading ECS cluster %s", clusterName) + out, err := conn.DescribeClusters(&ecs.DescribeClustersInput{ + Clusters: []*string{aws.String(clusterName)}, + }) + if err != nil { + return err + } + log.Printf("[DEBUG] Received ECS clusters: %#v", out.Clusters) + + d.SetId(*out.Clusters[0].ClusterARN) + d.Set("name", *out.Clusters[0].ClusterName) + + return nil +} + +func resourceAwsEcsClusterDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ecsconn + + log.Printf("[DEBUG] Deleting ECS cluster %s", d.Id()) + + // TODO: Handle ClientException: The Cluster cannot be deleted while Container Instances are active. + // TODO: Handle ClientException: The Cluster cannot be deleted while Services are active. + + out, err := conn.DeleteCluster(&ecs.DeleteClusterInput{ + Cluster: aws.String(d.Id()), + }) + + log.Printf("[DEBUG] ECS cluster %s deleted: %#v", d.Id(), out) + + return err +} diff --git a/builtin/providers/aws/resource_aws_ecs_cluster_test.go b/builtin/providers/aws/resource_aws_ecs_cluster_test.go new file mode 100644 index 000000000..308085d1d --- /dev/null +++ b/builtin/providers/aws/resource_aws_ecs_cluster_test.go @@ -0,0 +1,68 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSEcsCluster_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsClusterDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSEcsCluster, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsClusterExists("aws_ecs_cluster.foo"), + ), + }, + }, + }) +} + +func testAccCheckAWSEcsClusterDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ecsconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ecs_cluster" { + continue + } + + out, err := conn.DescribeClusters(&ecs.DescribeClustersInput{ + Clusters: []*string{aws.String(rs.Primary.ID)}, + }) + + if err == nil { + if len(out.Clusters) != 0 { + return fmt.Errorf("ECS cluster still exists:\n%#v", out.Clusters) + } + } + + return err + } + + return nil +} + +func testAccCheckAWSEcsClusterExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + return nil + } +} + +var testAccAWSEcsCluster = ` +resource "aws_ecs_cluster" "foo" { + name = "red-grapes" +} +` diff --git a/builtin/providers/aws/resource_aws_ecs_service.go b/builtin/providers/aws/resource_aws_ecs_service.go new file mode 100644 index 000000000..c93296f41 --- /dev/null +++ b/builtin/providers/aws/resource_aws_ecs_service.go @@ -0,0 +1,316 @@ +package aws + +import ( + "bytes" + "fmt" + "log" + "regexp" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +var taskDefinitionRE = regexp.MustCompile("^([a-zA-Z0-9_-]+):([0-9]+)$") + +func resourceAwsEcsService() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsEcsServiceCreate, + Read: resourceAwsEcsServiceRead, + Update: resourceAwsEcsServiceUpdate, + Delete: resourceAwsEcsServiceDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "cluster": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "task_definition": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "desired_count": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + + "iam_role": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "load_balancer": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "elb_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "container_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "container_port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + }, + }, + Set: resourceAwsEcsLoadBalancerHash, + }, + }, + } +} + +func resourceAwsEcsServiceCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ecsconn + + input := ecs.CreateServiceInput{ + ServiceName: aws.String(d.Get("name").(string)), + TaskDefinition: aws.String(d.Get("task_definition").(string)), + DesiredCount: aws.Long(int64(d.Get("desired_count").(int))), + } + + if v, ok := d.GetOk("cluster"); ok { + input.Cluster = aws.String(v.(string)) + } + + loadBalancers := expandEcsLoadBalancers(d.Get("load_balancer").(*schema.Set).List()) + if len(loadBalancers) > 0 { + log.Printf("[DEBUG] Adding ECS load balancers: %#v", loadBalancers) + input.LoadBalancers = loadBalancers + } + if v, ok := d.GetOk("iam_role"); ok { + input.Role = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Creating ECS service: %#v", input) + out, err := conn.CreateService(&input) + if err != nil { + return err + } + + service := *out.Service + + log.Printf("[DEBUG] ECS service created: %s", *service.ServiceARN) + d.SetId(*service.ServiceARN) + d.Set("cluster", *service.ClusterARN) + + return resourceAwsEcsServiceUpdate(d, meta) +} + +func resourceAwsEcsServiceRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ecsconn + + log.Printf("[DEBUG] Reading ECS service %s", d.Id()) + input := ecs.DescribeServicesInput{ + Services: []*string{aws.String(d.Id())}, + Cluster: aws.String(d.Get("cluster").(string)), + } + + out, err := conn.DescribeServices(&input) + if err != nil { + return err + } + + service := out.Services[0] + log.Printf("[DEBUG] Received ECS service %#v", service) + + d.SetId(*service.ServiceARN) + d.Set("name", *service.ServiceName) + + // Save task definition in the same format + if strings.HasPrefix(d.Get("task_definition").(string), "arn:aws:ecs:") { + d.Set("task_definition", *service.TaskDefinition) + } else { + taskDefinition := buildFamilyAndRevisionFromARN(*service.TaskDefinition) + d.Set("task_definition", taskDefinition) + } + + d.Set("desired_count", *service.DesiredCount) + d.Set("cluster", *service.ClusterARN) + + if service.RoleARN != nil { + d.Set("iam_role", *service.RoleARN) + } + + if service.LoadBalancers != nil { + d.Set("load_balancers", flattenEcsLoadBalancers(service.LoadBalancers)) + } + + return nil +} + +func resourceAwsEcsServiceUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ecsconn + + log.Printf("[DEBUG] Updating ECS service %s", d.Id()) + input := ecs.UpdateServiceInput{ + Service: aws.String(d.Id()), + Cluster: aws.String(d.Get("cluster").(string)), + } + + if d.HasChange("desired_count") { + _, n := d.GetChange("desired_count") + input.DesiredCount = aws.Long(int64(n.(int))) + } + if d.HasChange("task_definition") { + _, n := d.GetChange("task_definition") + input.TaskDefinition = aws.String(n.(string)) + } + + out, err := conn.UpdateService(&input) + if err != nil { + return err + } + service := out.Service + log.Printf("[DEBUG] Updated ECS service %#v", service) + + return resourceAwsEcsServiceRead(d, meta) +} + +func resourceAwsEcsServiceDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ecsconn + + // Check if it's not already gone + resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{ + Services: []*string{aws.String(d.Id())}, + Cluster: aws.String(d.Get("cluster").(string)), + }) + if err != nil { + return err + } + log.Printf("[DEBUG] ECS service %s is currently %s", d.Id(), *resp.Services[0].Status) + + if *resp.Services[0].Status == "INACTIVE" { + return nil + } + + // Drain the ECS service + if *resp.Services[0].Status != "DRAINING" { + log.Printf("[DEBUG] Draining ECS service %s", d.Id()) + _, err = conn.UpdateService(&ecs.UpdateServiceInput{ + Service: aws.String(d.Id()), + Cluster: aws.String(d.Get("cluster").(string)), + DesiredCount: aws.Long(int64(0)), + }) + if err != nil { + return err + } + } + + input := ecs.DeleteServiceInput{ + Service: aws.String(d.Id()), + Cluster: aws.String(d.Get("cluster").(string)), + } + + log.Printf("[DEBUG] Deleting ECS service %#v", input) + out, err := conn.DeleteService(&input) + if err != nil { + return err + } + + // Wait until it's deleted + wait := resource.StateChangeConf{ + Pending: []string{"DRAINING"}, + Target: "INACTIVE", + Timeout: 5 * time.Minute, + MinTimeout: 1 * time.Second, + Refresh: func() (interface{}, string, error) { + log.Printf("[DEBUG] Checking if ECS service %s is INACTIVE", d.Id()) + resp, err := conn.DescribeServices(&ecs.DescribeServicesInput{ + Services: []*string{aws.String(d.Id())}, + Cluster: aws.String(d.Get("cluster").(string)), + }) + if err != nil { + return resp, "FAILED", err + } + + return resp, *resp.Services[0].Status, nil + }, + } + + _, err = wait.WaitForState() + if err != nil { + return err + } + + log.Printf("[DEBUG] ECS service %s deleted.", *out.Service.ServiceARN) + return nil +} + +func resourceAwsEcsLoadBalancerHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["elb_name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["container_name"].(string))) + buf.WriteString(fmt.Sprintf("%d-", m["container_port"].(int))) + + return hashcode.String(buf.String()) +} + +func buildFamilyAndRevisionFromARN(arn string) string { + return strings.Split(arn, "/")[1] +} + +func buildTaskDefinitionARN(taskDefinition string, meta interface{}) (string, error) { + // If it's already an ARN, just return it + if strings.HasPrefix(taskDefinition, "arn:aws:ecs:") { + return taskDefinition, nil + } + + // Parse out family & revision + family, revision, err := parseTaskDefinition(taskDefinition) + if err != nil { + return "", err + } + + iamconn := meta.(*AWSClient).iamconn + region := meta.(*AWSClient).region + + // An zero value GetUserInput{} defers to the currently logged in user + resp, err := iamconn.GetUser(&iam.GetUserInput{}) + if err != nil { + return "", fmt.Errorf("GetUser ERROR: %#v", err) + } + + // arn:aws:iam::0123456789:user/username + userARN := *resp.User.ARN + accountID := strings.Split(userARN, ":")[4] + + // arn:aws:ecs:us-west-2:01234567890:task-definition/mongodb:3 + arn := fmt.Sprintf("arn:aws:ecs:%s:%s:task-definition/%s:%s", + region, accountID, family, revision) + log.Printf("[DEBUG] Built task definition ARN: %s", arn) + return arn, nil +} + +func parseTaskDefinition(taskDefinition string) (string, string, error) { + matches := taskDefinitionRE.FindAllStringSubmatch(taskDefinition, 2) + + if len(matches) == 0 || len(matches[0]) != 3 { + return "", "", fmt.Errorf( + "Invalid task definition format, family:rev or ARN expected (%#v)", + taskDefinition) + } + + return matches[0][1], matches[0][2], nil +} diff --git a/builtin/providers/aws/resource_aws_ecs_service_test.go b/builtin/providers/aws/resource_aws_ecs_service_test.go new file mode 100644 index 000000000..4257fd365 --- /dev/null +++ b/builtin/providers/aws/resource_aws_ecs_service_test.go @@ -0,0 +1,276 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestParseTaskDefinition(t *testing.T) { + cases := map[string]map[string]interface{}{ + "invalid": map[string]interface{}{ + "family": "", + "revision": "", + "isValid": false, + }, + "invalidWithColon:": map[string]interface{}{ + "family": "", + "revision": "", + "isValid": false, + }, + "1234": map[string]interface{}{ + "family": "", + "revision": "", + "isValid": false, + }, + "invalid:aaa": map[string]interface{}{ + "family": "", + "revision": "", + "isValid": false, + }, + "invalid=family:1": map[string]interface{}{ + "family": "", + "revision": "", + "isValid": false, + }, + "invalid:name:1": map[string]interface{}{ + "family": "", + "revision": "", + "isValid": false, + }, + "valid:1": map[string]interface{}{ + "family": "valid", + "revision": "1", + "isValid": true, + }, + "abc12-def:54": map[string]interface{}{ + "family": "abc12-def", + "revision": "54", + "isValid": true, + }, + "lorem_ip-sum:123": map[string]interface{}{ + "family": "lorem_ip-sum", + "revision": "123", + "isValid": true, + }, + "lorem-ipsum:1": map[string]interface{}{ + "family": "lorem-ipsum", + "revision": "1", + "isValid": true, + }, + } + + for input, expectedOutput := range cases { + family, revision, err := parseTaskDefinition(input) + isValid := expectedOutput["isValid"].(bool) + if !isValid && err == nil { + t.Fatalf("Task definition %s should fail", input) + } + + expectedFamily := expectedOutput["family"].(string) + if family != expectedFamily { + t.Fatalf("Unexpected family (%#v) for task definition %s\n%#v", family, input, err) + } + expectedRevision := expectedOutput["revision"].(string) + if revision != expectedRevision { + t.Fatalf("Unexpected revision (%#v) for task definition %s\n%#v", revision, input, err) + } + } +} + +func TestAccAWSEcsServiceWithARN(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsServiceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSEcsService, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"), + ), + }, + + resource.TestStep{ + Config: testAccAWSEcsServiceModified, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"), + ), + }, + }, + }) +} + +func TestAccAWSEcsServiceWithFamilyAndRevision(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsServiceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSEcsServiceWithFamilyAndRevision, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsServiceExists("aws_ecs_service.jenkins"), + ), + }, + + resource.TestStep{ + Config: testAccAWSEcsServiceWithFamilyAndRevisionModified, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsServiceExists("aws_ecs_service.jenkins"), + ), + }, + }, + }) +} + +func testAccCheckAWSEcsServiceDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ecsconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ecs_service" { + continue + } + + out, err := conn.DescribeServices(&ecs.DescribeServicesInput{ + Services: []*string{aws.String(rs.Primary.ID)}, + }) + + if err == nil { + if len(out.Services) > 0 { + return fmt.Errorf("ECS service still exists:\n%#v", out.Services) + } + } + + return err + } + + return nil +} + +func testAccCheckAWSEcsServiceExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + return nil + } +} + +var testAccAWSEcsService = ` +resource "aws_ecs_cluster" "default" { + name = "terraformecstest1" +} + +resource "aws_ecs_task_definition" "mongo" { + family = "mongodb" + container_definitions = < 0 { - attrmap := *attributeOutput.Attributes + if attributeOutput.Attributes != nil && len(attributeOutput.Attributes) > 0 { + attrmap := attributeOutput.Attributes resource := *resourceAwsSnsTopic() // iKey = internal struct key, oKey = AWS Attribute Map key for iKey, oKey := range SNSAttributeMap { @@ -150,4 +149,4 @@ func resourceAwsSnsTopicDelete(d *schema.ResourceData, meta interface{}) error { return err } return nil -} \ No newline at end of file +} diff --git a/builtin/providers/aws/resource_aws_sns_topic_subscription.go b/builtin/providers/aws/resource_aws_sns_topic_subscription.go index fd8c67604..01926e895 100644 --- a/builtin/providers/aws/resource_aws_sns_topic_subscription.go +++ b/builtin/providers/aws/resource_aws_sns_topic_subscription.go @@ -6,11 +6,10 @@ import ( "github.com/hashicorp/terraform/helper/schema" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/sns" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sns" ) - func resourceAwsSnsTopicSubscription() *schema.Resource { return &schema.Resource{ Create: resourceAwsSnsTopicSubscriptionCreate, @@ -25,29 +24,29 @@ func resourceAwsSnsTopicSubscription() *schema.Resource { ForceNew: false, }, "endpoint": &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: false, + Type: schema.TypeString, + Required: true, + ForceNew: false, }, "topic_arn": &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: false, + Type: schema.TypeString, + Required: true, + ForceNew: false, }, "delivery_policy": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: false, + Type: schema.TypeString, + Optional: true, + ForceNew: false, }, "raw_message_delivery": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - ForceNew: false, - Default: false, + Type: schema.TypeBool, + Optional: true, + ForceNew: false, + Default: false, }, "arn": &schema.Schema{ - Type: schema.TypeString, - Computed: true, + Type: schema.TypeString, + Computed: true, }, }, } @@ -56,7 +55,7 @@ func resourceAwsSnsTopicSubscription() *schema.Resource { func resourceAwsSnsTopicSubscriptionCreate(d *schema.ResourceData, meta interface{}) error { snsconn := meta.(*AWSClient).snsconn - if(d.Get("protocol") == "email") { + if d.Get("protocol") == "email" { return fmt.Errorf("Email endpoints are not supported!") } @@ -107,8 +106,8 @@ func resourceAwsSnsTopicSubscriptionUpdate(d *schema.ResourceData, meta interfac req := &sns.SetSubscriptionAttributesInput{ SubscriptionARN: aws.String(d.Id()), - AttributeName: aws.String("RawMessageDelivery"), - AttributeValue: aws.String(attrValue), + AttributeName: aws.String("RawMessageDelivery"), + AttributeValue: aws.String(attrValue), } _, err := snsconn.SetSubscriptionAttributes(req) @@ -132,8 +131,8 @@ func resourceAwsSnsTopicSubscriptionRead(d *schema.ResourceData, meta interface{ return err } - if attributeOutput.Attributes != nil && len(*attributeOutput.Attributes) > 0 { - attrHash := *attributeOutput.Attributes + if attributeOutput.Attributes != nil && len(attributeOutput.Attributes) > 0 { + attrHash := attributeOutput.Attributes log.Printf("[DEBUG] raw message delivery: %s", *attrHash["RawMessageDelivery"]) if *attrHash["RawMessageDelivery"] == "true" { d.Set("raw_message_delivery", true) @@ -158,7 +157,7 @@ func resourceAwsSnsTopicSubscriptionDelete(d *schema.ResourceData, meta interfac return nil } -func subscribeToSNSTopic(d *schema.ResourceData, snsconn *sns.SNS) (output *sns.SubscribeOutput, err error) { +func subscribeToSNSTopic(d *schema.ResourceData, snsconn *sns.SNS) (output *sns.SubscribeOutput, err error) { protocol := d.Get("protocol").(string) endpoint := d.Get("endpoint").(string) topic_arn := d.Get("topic_arn").(string) @@ -178,4 +177,4 @@ func subscribeToSNSTopic(d *schema.ResourceData, snsconn *sns.SNS) (output *sns. log.Printf("[DEBUG] Created new subscription!") return output, nil -} \ No newline at end of file +} diff --git a/builtin/providers/aws/resource_aws_sns_topic_subscription_test.go b/builtin/providers/aws/resource_aws_sns_topic_subscription_test.go index ada177ccd..3ab538bcd 100644 --- a/builtin/providers/aws/resource_aws_sns_topic_subscription_test.go +++ b/builtin/providers/aws/resource_aws_sns_topic_subscription_test.go @@ -4,14 +4,14 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/sns" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sns" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "github.com/awslabs/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/awserr" ) -func TestAccAWSSNSTopicSubscription(t *testing.T) { +func TestAccAWSSNSTopicSubscription_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -100,4 +100,4 @@ resource "aws_sns_topic_subscription" "test_subscription" { protocol = "sqs" endpoint = "${aws_sqs_queue.test_queue.arn}" } -` \ No newline at end of file +` diff --git a/builtin/providers/aws/resource_aws_sns_topic_test.go b/builtin/providers/aws/resource_aws_sns_topic_test.go index f13c0b677..c716d4fb6 100644 --- a/builtin/providers/aws/resource_aws_sns_topic_test.go +++ b/builtin/providers/aws/resource_aws_sns_topic_test.go @@ -4,14 +4,14 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/sns" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sns" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "github.com/awslabs/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/awserr" ) -func TestAccAWSSNSTopic(t *testing.T) { +func TestAccAWSSNSTopic_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -86,4 +86,4 @@ const testAccAWSSNSTopicConfig = ` resource "aws_sns_topic" "test_topic" { name = "terraform-test-topic" } -` \ No newline at end of file +` diff --git a/builtin/providers/aws/resource_aws_sqs_queue.go b/builtin/providers/aws/resource_aws_sqs_queue.go index 7c7473f04..0e85f7cbe 100644 --- a/builtin/providers/aws/resource_aws_sqs_queue.go +++ b/builtin/providers/aws/resource_aws_sqs_queue.go @@ -7,8 +7,8 @@ import ( "github.com/hashicorp/terraform/helper/schema" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/sqs" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sqs" ) var AttributeMap = map[string]string{ @@ -108,7 +108,7 @@ func resourceAwsSqsQueueCreate(d *schema.ResourceData, meta interface{}) error { } if len(attributes) > 0 { - req.Attributes = &attributes + req.Attributes = attributes } output, err := sqsconn.CreateQueue(req) @@ -144,7 +144,7 @@ func resourceAwsSqsQueueUpdate(d *schema.ResourceData, meta interface{}) error { if len(attributes) > 0 { req := &sqs.SetQueueAttributesInput{ QueueURL: aws.String(d.Id()), - Attributes: &attributes, + Attributes: attributes, } sqsconn.SetQueueAttributes(req) } @@ -164,8 +164,8 @@ func resourceAwsSqsQueueRead(d *schema.ResourceData, meta interface{}) error { return err } - if attributeOutput.Attributes != nil && len(*attributeOutput.Attributes) > 0 { - attrmap := *attributeOutput.Attributes + if attributeOutput.Attributes != nil && len(attributeOutput.Attributes) > 0 { + attrmap := attributeOutput.Attributes resource := *resourceAwsSqsQueue() // iKey = internal struct key, oKey = AWS Attribute Map key for iKey, oKey := range AttributeMap { diff --git a/builtin/providers/aws/resource_aws_sqs_queue_test.go b/builtin/providers/aws/resource_aws_sqs_queue_test.go index f6d990c3a..e340a05b9 100644 --- a/builtin/providers/aws/resource_aws_sqs_queue_test.go +++ b/builtin/providers/aws/resource_aws_sqs_queue_test.go @@ -4,14 +4,14 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/sqs" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/sqs" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) -func TestAccAWSSQSQueue(t *testing.T) { +func TestAccAWSSQSQueue_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -84,7 +84,7 @@ func testAccCheckAWSSQSExistsWithDefaults(n string) resource.TestCheckFunc { } // checking if attributes are defaults - for k, v := range *resp.Attributes { + for k, v := range resp.Attributes { if k == "VisibilityTimeout" && *v != "30" { return fmt.Errorf("VisibilityTimeout (%s) was not set to 30", *v) } @@ -134,7 +134,7 @@ func testAccCheckAWSSQSExistsWithOverrides(n string) resource.TestCheckFunc { } // checking if attributes match our overrides - for k, v := range *resp.Attributes { + for k, v := range resp.Attributes { if k == "VisibilityTimeout" && *v != "60" { return fmt.Errorf("VisibilityTimeout (%s) was not set to 60", *v) } diff --git a/builtin/providers/aws/resource_aws_subnet.go b/builtin/providers/aws/resource_aws_subnet.go index f6d56681a..326ddc28d 100644 --- a/builtin/providers/aws/resource_aws_subnet.go +++ b/builtin/providers/aws/resource_aws_subnet.go @@ -5,9 +5,9 @@ import ( "log" "time" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/resource_aws_subnet_test.go b/builtin/providers/aws/resource_aws_subnet_test.go index 206333d41..78dbf2d9f 100644 --- a/builtin/providers/aws/resource_aws_subnet_test.go +++ b/builtin/providers/aws/resource_aws_subnet_test.go @@ -4,14 +4,14 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) -func TestAccAWSSubnet(t *testing.T) { +func TestAccAWSSubnet_basic(t *testing.T) { var v ec2.Subnet testCheck := func(*terraform.State) error { diff --git a/builtin/providers/aws/resource_aws_volume_attachment.go b/builtin/providers/aws/resource_aws_volume_attachment.go index cb9374968..6822ebd28 100644 --- a/builtin/providers/aws/resource_aws_volume_attachment.go +++ b/builtin/providers/aws/resource_aws_volume_attachment.go @@ -6,9 +6,9 @@ import ( "log" "time" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" diff --git a/builtin/providers/aws/resource_aws_volume_attachment_test.go b/builtin/providers/aws/resource_aws_volume_attachment_test.go index 22df8c085..66807884f 100644 --- a/builtin/providers/aws/resource_aws_volume_attachment_test.go +++ b/builtin/providers/aws/resource_aws_volume_attachment_test.go @@ -5,7 +5,7 @@ import ( "log" "testing" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) diff --git a/builtin/providers/aws/resource_aws_vpc.go b/builtin/providers/aws/resource_aws_vpc.go index b13c1521b..2ee2473b2 100644 --- a/builtin/providers/aws/resource_aws_vpc.go +++ b/builtin/providers/aws/resource_aws_vpc.go @@ -5,9 +5,9 @@ import ( "log" "time" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/resource_aws_vpc_dhcp_options.go b/builtin/providers/aws/resource_aws_vpc_dhcp_options.go index 4fd2d24f4..540c62802 100644 --- a/builtin/providers/aws/resource_aws_vpc_dhcp_options.go +++ b/builtin/providers/aws/resource_aws_vpc_dhcp_options.go @@ -6,9 +6,9 @@ import ( "strings" "time" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/resource_aws_vpc_dhcp_options_association.go b/builtin/providers/aws/resource_aws_vpc_dhcp_options_association.go index 46c447213..93ac4494e 100644 --- a/builtin/providers/aws/resource_aws_vpc_dhcp_options_association.go +++ b/builtin/providers/aws/resource_aws_vpc_dhcp_options_association.go @@ -3,8 +3,8 @@ package aws import ( "log" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/resource_aws_vpc_dhcp_options_association_test.go b/builtin/providers/aws/resource_aws_vpc_dhcp_options_association_test.go index 1b00ce3dc..f40f1a06e 100644 --- a/builtin/providers/aws/resource_aws_vpc_dhcp_options_association_test.go +++ b/builtin/providers/aws/resource_aws_vpc_dhcp_options_association_test.go @@ -4,12 +4,12 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) -func TestAccAWSDHCPOptionsAssociation(t *testing.T) { +func TestAccAWSDHCPOptionsAssociation_basic(t *testing.T) { var v ec2.VPC var d ec2.DHCPOptions diff --git a/builtin/providers/aws/resource_aws_vpc_dhcp_options_test.go b/builtin/providers/aws/resource_aws_vpc_dhcp_options_test.go index 221ba99fc..988c8b3c4 100644 --- a/builtin/providers/aws/resource_aws_vpc_dhcp_options_test.go +++ b/builtin/providers/aws/resource_aws_vpc_dhcp_options_test.go @@ -4,14 +4,14 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) -func TestAccDHCPOptions(t *testing.T) { +func TestAccDHCPOptions_basic(t *testing.T) { var d ec2.DHCPOptions resource.Test(t, resource.TestCase{ diff --git a/builtin/providers/aws/resource_aws_vpc_peering_connection.go b/builtin/providers/aws/resource_aws_vpc_peering_connection.go index 9a277a92b..2be170682 100644 --- a/builtin/providers/aws/resource_aws_vpc_peering_connection.go +++ b/builtin/providers/aws/resource_aws_vpc_peering_connection.go @@ -5,9 +5,9 @@ import ( "log" "time" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/resource_aws_vpc_peering_connection_test.go b/builtin/providers/aws/resource_aws_vpc_peering_connection_test.go index a840288c9..3b7896e40 100644 --- a/builtin/providers/aws/resource_aws_vpc_peering_connection_test.go +++ b/builtin/providers/aws/resource_aws_vpc_peering_connection_test.go @@ -5,13 +5,13 @@ import ( "os" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) -func TestAccAWSVPCPeeringConnection_normal(t *testing.T) { +func TestAccAWSVPCPeeringConnection_basic(t *testing.T) { var connection ec2.VPCPeeringConnection resource.Test(t, resource.TestCase{ diff --git a/builtin/providers/aws/resource_aws_vpc_test.go b/builtin/providers/aws/resource_aws_vpc_test.go index 8ee7820aa..95121ec6f 100644 --- a/builtin/providers/aws/resource_aws_vpc_test.go +++ b/builtin/providers/aws/resource_aws_vpc_test.go @@ -4,9 +4,9 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) diff --git a/builtin/providers/aws/resource_aws_vpn_connection.go b/builtin/providers/aws/resource_aws_vpn_connection.go index 5780c887d..a97cd7559 100644 --- a/builtin/providers/aws/resource_aws_vpn_connection.go +++ b/builtin/providers/aws/resource_aws_vpn_connection.go @@ -6,9 +6,9 @@ import ( "log" "time" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" diff --git a/builtin/providers/aws/resource_aws_vpn_connection_test.go b/builtin/providers/aws/resource_aws_vpn_connection_test.go index 6b61d97e4..55dbb447c 100644 --- a/builtin/providers/aws/resource_aws_vpn_connection_test.go +++ b/builtin/providers/aws/resource_aws_vpn_connection_test.go @@ -4,14 +4,14 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) -func TestAccAwsVpnConnection(t *testing.T) { +func TestAccAwsVpnConnection_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, diff --git a/builtin/providers/aws/resource_aws_vpn_gateway.go b/builtin/providers/aws/resource_aws_vpn_gateway.go index b909a460d..1a331e946 100644 --- a/builtin/providers/aws/resource_aws_vpn_gateway.go +++ b/builtin/providers/aws/resource_aws_vpn_gateway.go @@ -5,9 +5,9 @@ import ( "log" "time" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/resource_aws_vpn_gateway_test.go b/builtin/providers/aws/resource_aws_vpn_gateway_test.go index 1643da86c..6b1d3fc48 100644 --- a/builtin/providers/aws/resource_aws_vpn_gateway_test.go +++ b/builtin/providers/aws/resource_aws_vpn_gateway_test.go @@ -4,9 +4,9 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) diff --git a/builtin/providers/aws/resource_vpn_connection_route.go b/builtin/providers/aws/resource_vpn_connection_route.go index 198f730d2..b7bd9bd8c 100644 --- a/builtin/providers/aws/resource_vpn_connection_route.go +++ b/builtin/providers/aws/resource_vpn_connection_route.go @@ -5,9 +5,9 @@ import ( "log" "strings" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/resource_vpn_connection_route_test.go b/builtin/providers/aws/resource_vpn_connection_route_test.go index 55779898c..955aa5f1a 100644 --- a/builtin/providers/aws/resource_vpn_connection_route_test.go +++ b/builtin/providers/aws/resource_vpn_connection_route_test.go @@ -4,14 +4,14 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) -func TestAccAwsVpnConnectionRoute(t *testing.T) { +func TestAccAwsVpnConnectionRoute_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, diff --git a/builtin/providers/aws/s3_tags.go b/builtin/providers/aws/s3_tags.go index 01782f238..5d620b132 100644 --- a/builtin/providers/aws/s3_tags.go +++ b/builtin/providers/aws/s3_tags.go @@ -3,9 +3,9 @@ package aws import ( "log" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/s3" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/s3_tags_test.go b/builtin/providers/aws/s3_tags_test.go index 70c76cf0d..3d4188c5f 100644 --- a/builtin/providers/aws/s3_tags_test.go +++ b/builtin/providers/aws/s3_tags_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/awslabs/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) diff --git a/builtin/providers/aws/structure.go b/builtin/providers/aws/structure.go index 269734aa7..e6c0534fd 100644 --- a/builtin/providers/aws/structure.go +++ b/builtin/providers/aws/structure.go @@ -1,15 +1,18 @@ package aws import ( + "bytes" + "encoding/json" "fmt" "sort" "strings" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/ec2" - "github.com/awslabs/aws-sdk-go/service/elb" - "github.com/awslabs/aws-sdk-go/service/rds" - "github.com/awslabs/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/elb" + "github.com/aws/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/service/route53" "github.com/hashicorp/terraform/helper/schema" ) @@ -42,6 +45,64 @@ func expandListeners(configured []interface{}) ([]*elb.Listener, error) { return listeners, nil } +// Takes the result of flatmap. Expand for an array of listeners and +// returns ECS Volume compatible objects +func expandEcsVolumes(configured []interface{}) ([]*ecs.Volume, error) { + volumes := make([]*ecs.Volume, 0, len(configured)) + + // Loop over our configured volumes and create + // an array of aws-sdk-go compatible objects + for _, lRaw := range configured { + data := lRaw.(map[string]interface{}) + + l := &ecs.Volume{ + Name: aws.String(data["name"].(string)), + Host: &ecs.HostVolumeProperties{ + SourcePath: aws.String(data["host_path"].(string)), + }, + } + + volumes = append(volumes, l) + } + + return volumes, nil +} + +// Takes JSON in a string. Decodes JSON into +// an array of ecs.ContainerDefinition compatible objects +func expandEcsContainerDefinitions(rawDefinitions string) ([]*ecs.ContainerDefinition, error) { + var definitions []*ecs.ContainerDefinition + + err := json.Unmarshal([]byte(rawDefinitions), &definitions) + if err != nil { + return nil, fmt.Errorf("Error decoding JSON: %s", err) + } + + return definitions, nil +} + +// Takes the result of flatmap. Expand for an array of load balancers and +// returns ecs.LoadBalancer compatible objects +func expandEcsLoadBalancers(configured []interface{}) []*ecs.LoadBalancer { + loadBalancers := make([]*ecs.LoadBalancer, 0, len(configured)) + + // Loop over our configured load balancers and create + // an array of aws-sdk-go compatible objects + for _, lRaw := range configured { + data := lRaw.(map[string]interface{}) + + l := &ecs.LoadBalancer{ + ContainerName: aws.String(data["container_name"].(string)), + ContainerPort: aws.Long(int64(data["container_port"].(int))), + LoadBalancerName: aws.String(data["elb_name"].(string)), + } + + loadBalancers = append(loadBalancers, l) + } + + return loadBalancers +} + // Takes the result of flatmap.Expand for an array of ingress/egress security // group rules and returns EC2 API compatible objects. This function will error // if it finds invalid permissions input, namely a protocol of "-1" with either @@ -215,6 +276,44 @@ func flattenListeners(list []*elb.ListenerDescription) []map[string]interface{} return result } +// Flattens an array of Volumes into a []map[string]interface{} +func flattenEcsVolumes(list []*ecs.Volume) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(list)) + for _, volume := range list { + l := map[string]interface{}{ + "name": *volume.Name, + "host_path": *volume.Host.SourcePath, + } + result = append(result, l) + } + return result +} + +// Flattens an array of ECS LoadBalancers into a []map[string]interface{} +func flattenEcsLoadBalancers(list []*ecs.LoadBalancer) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(list)) + for _, loadBalancer := range list { + l := map[string]interface{}{ + "elb_name": *loadBalancer.LoadBalancerName, + "container_name": *loadBalancer.ContainerName, + "container_port": *loadBalancer.ContainerPort, + } + result = append(result, l) + } + return result +} + +// Encodes an array of ecs.ContainerDefinitions into a JSON string +func flattenEcsContainerDefinitions(definitions []*ecs.ContainerDefinition) (string, error) { + byteArray, err := json.Marshal(definitions) + if err != nil { + return "", fmt.Errorf("Error encoding to JSON: %s", err) + } + + n := bytes.Index(byteArray, []byte{0}) + return string(byteArray[:n]), nil +} + // Flattens an array of Parameters into a []map[string]interface{} func flattenParameters(list []*rds.Parameter) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(list)) diff --git a/builtin/providers/aws/structure_test.go b/builtin/providers/aws/structure_test.go index ace216e6b..765776f76 100644 --- a/builtin/providers/aws/structure_test.go +++ b/builtin/providers/aws/structure_test.go @@ -4,11 +4,11 @@ import ( "reflect" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/ec2" - "github.com/awslabs/aws-sdk-go/service/elb" - "github.com/awslabs/aws-sdk-go/service/rds" - "github.com/awslabs/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/elb" + "github.com/aws/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/service/route53" "github.com/hashicorp/terraform/flatmap" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/tags.go b/builtin/providers/aws/tags.go index 88934c799..4fc1b8d1d 100644 --- a/builtin/providers/aws/tags.go +++ b/builtin/providers/aws/tags.go @@ -3,8 +3,8 @@ package aws import ( "log" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/tagsEC.go b/builtin/providers/aws/tagsEC.go index 55aa2c4e5..3a5ba1acf 100644 --- a/builtin/providers/aws/tagsEC.go +++ b/builtin/providers/aws/tagsEC.go @@ -3,8 +3,8 @@ package aws import ( "log" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/elasticache" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elasticache" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/tagsEC_test.go b/builtin/providers/aws/tagsEC_test.go index dc61bd0cc..11737ecea 100644 --- a/builtin/providers/aws/tagsEC_test.go +++ b/builtin/providers/aws/tagsEC_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/awslabs/aws-sdk-go/service/elasticache" + "github.com/aws/aws-sdk-go/service/elasticache" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) diff --git a/builtin/providers/aws/tagsELB.go b/builtin/providers/aws/tagsELB.go index 0513848cc..1b1a93488 100644 --- a/builtin/providers/aws/tagsELB.go +++ b/builtin/providers/aws/tagsELB.go @@ -3,8 +3,8 @@ package aws import ( "log" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/elb" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elb" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/tagsELB_test.go b/builtin/providers/aws/tagsELB_test.go index b2078a8d9..9ec6dd8db 100644 --- a/builtin/providers/aws/tagsELB_test.go +++ b/builtin/providers/aws/tagsELB_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/awslabs/aws-sdk-go/service/elb" + "github.com/aws/aws-sdk-go/service/elb" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) diff --git a/builtin/providers/aws/tagsRDS.go b/builtin/providers/aws/tagsRDS.go index 0a1a39670..3e4e0c700 100644 --- a/builtin/providers/aws/tagsRDS.go +++ b/builtin/providers/aws/tagsRDS.go @@ -3,8 +3,8 @@ package aws import ( "log" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/tagsRDS_test.go b/builtin/providers/aws/tagsRDS_test.go index 764f23e23..2aa0a05b3 100644 --- a/builtin/providers/aws/tagsRDS_test.go +++ b/builtin/providers/aws/tagsRDS_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/awslabs/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) diff --git a/builtin/providers/aws/tags_route53.go b/builtin/providers/aws/tags_route53.go index 66936d1f9..93847669e 100644 --- a/builtin/providers/aws/tags_route53.go +++ b/builtin/providers/aws/tags_route53.go @@ -3,8 +3,8 @@ package aws import ( "log" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/builtin/providers/aws/tags_route53_test.go b/builtin/providers/aws/tags_route53_test.go index 428629076..a0b2c6410 100644 --- a/builtin/providers/aws/tags_route53_test.go +++ b/builtin/providers/aws/tags_route53_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/awslabs/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go/service/route53" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) diff --git a/builtin/providers/aws/tags_test.go b/builtin/providers/aws/tags_test.go index 5bbc3b78f..8aac147fc 100644 --- a/builtin/providers/aws/tags_test.go +++ b/builtin/providers/aws/tags_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) diff --git a/builtin/providers/aws/website_endpoint_url_test.go b/builtin/providers/aws/website_endpoint_url_test.go index 38b11d329..2f4ce5249 100644 --- a/builtin/providers/aws/website_endpoint_url_test.go +++ b/builtin/providers/aws/website_endpoint_url_test.go @@ -2,16 +2,27 @@ package aws import "testing" -func TestWebsiteEndpointUrl_withoutRegion(t *testing.T) { - u := WebsiteEndpointUrl("buck.et", "") - if u != "buck.et.s3-website-us-east-1.amazonaws.com" { - t.Fatalf("bad: %s", u) - } +// http://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteEndpoints.html +var websiteEndpoints = []struct { + in string + out string +}{ + {"", "bucket-name.s3-website-us-east-1.amazonaws.com"}, + {"us-west-2", "bucket-name.s3-website-us-west-2.amazonaws.com"}, + {"us-west-1", "bucket-name.s3-website-us-west-1.amazonaws.com"}, + {"eu-west-1", "bucket-name.s3-website-eu-west-1.amazonaws.com"}, + {"eu-central-1", "bucket-name.s3-website.eu-central-1.amazonaws.com"}, + {"ap-southeast-1", "bucket-name.s3-website-ap-southeast-1.amazonaws.com"}, + {"ap-northeast-1", "bucket-name.s3-website-ap-northeast-1.amazonaws.com"}, + {"ap-southeast-2", "bucket-name.s3-website-ap-southeast-2.amazonaws.com"}, + {"sa-east-1", "bucket-name.s3-website-sa-east-1.amazonaws.com"}, } -func TestWebsiteEndpointUrl_withRegion(t *testing.T) { - u := WebsiteEndpointUrl("buck.et", "us-west-1") - if u != "buck.et.s3-website-us-west-1.amazonaws.com" { - t.Fatalf("bad: %s", u) +func TestWebsiteEndpointUrl(t *testing.T) { + for _, tt := range websiteEndpoints { + s := WebsiteEndpointUrl("bucket-name", tt.in) + if s != tt.out { + t.Errorf("WebsiteEndpointUrl(\"bucket-name\", %q) => %q, want %q", tt.in, s, tt.out) + } } } diff --git a/builtin/providers/azure/config.go b/builtin/providers/azure/config.go new file mode 100644 index 000000000..c21f6f705 --- /dev/null +++ b/builtin/providers/azure/config.go @@ -0,0 +1,58 @@ +package azure + +import ( + "fmt" + "os" + "sync" + + "github.com/svanharmelen/azure-sdk-for-go/management" +) + +// Config is the configuration structure used to instantiate a +// new Azure management client. +type Config struct { + SettingsFile string + SubscriptionID string + Certificate []byte + ManagementURL string +} + +// Client contains all the handles required for managing Azure services. +type Client struct { + // unfortunately; because of how Azure's network API works; doing networking operations + // concurrently is very hazardous, and we need a mutex to guard the management.Client. + mutex *sync.Mutex + mgmtClient management.Client +} + +// NewClientFromSettingsFile returns a new Azure management +// client created using a publish settings file. +func (c *Config) NewClientFromSettingsFile() (*Client, error) { + if _, err := os.Stat(c.SettingsFile); os.IsNotExist(err) { + return nil, fmt.Errorf("Publish Settings file %q does not exist!", c.SettingsFile) + } + + mc, err := management.ClientFromPublishSettingsFile(c.SettingsFile, c.SubscriptionID) + if err != nil { + return nil, nil + } + + return &Client{ + mutex: &sync.Mutex{}, + mgmtClient: mc, + }, nil +} + +// NewClient returns a new Azure management client created +// using a subscription ID and certificate. +func (c *Config) NewClient() (*Client, error) { + mc, err := management.NewClient(c.SubscriptionID, c.Certificate) + if err != nil { + return nil, nil + } + + return &Client{ + mutex: &sync.Mutex{}, + mgmtClient: mc, + }, nil +} diff --git a/builtin/providers/azure/provider.go b/builtin/providers/azure/provider.go new file mode 100644 index 000000000..eef0ca49c --- /dev/null +++ b/builtin/providers/azure/provider.go @@ -0,0 +1,68 @@ +package azure + +import ( + "fmt" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/go-homedir" +) + +// Provider returns a terraform.ResourceProvider. +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "settings_file": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("AZURE_SETTINGS_FILE", nil), + }, + + "subscription_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("AZURE_SUBSCRIPTION_ID", ""), + }, + + "certificate": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("AZURE_CERTIFICATE", ""), + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + "azure_data_disk": resourceAzureDataDisk(), + "azure_instance": resourceAzureInstance(), + "azure_security_group": resourceAzureSecurityGroup(), + "azure_virtual_network": resourceAzureVirtualNetwork(), + }, + + ConfigureFunc: providerConfigure, + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + settingsFile, err := homedir.Expand(d.Get("settings_file").(string)) + if err != nil { + return nil, fmt.Errorf("Error expanding the settings file path: %s", err) + } + + config := Config{ + SettingsFile: settingsFile, + SubscriptionID: d.Get("subscription_id").(string), + Certificate: []byte(d.Get("certificate").(string)), + } + + if config.SettingsFile != "" { + return config.NewClientFromSettingsFile() + } + + if config.SubscriptionID != "" && len(config.Certificate) > 0 { + return config.NewClient() + } + + return nil, fmt.Errorf( + "Insufficient configuration data. Please specify either a 'settings_file'\n" + + "or both a 'subscription_id' and 'certificate'.") +} diff --git a/builtin/providers/azure/provider_test.go b/builtin/providers/azure/provider_test.go new file mode 100644 index 000000000..5403df3ef --- /dev/null +++ b/builtin/providers/azure/provider_test.go @@ -0,0 +1,45 @@ +package azure + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "azure": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("AZURE_SETTINGS_FILE"); v == "" { + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + certificate := os.Getenv("AZURE_CERTIFICATE") + + if subscriptionID == "" || certificate == "" { + t.Fatal("either AZURE_SETTINGS_FILE, or AZURE_SUBSCRIPTION_ID " + + "and AZURE_CERTIFICATE must be set for acceptance tests") + } + } + + if v := os.Getenv("AZURE_STORAGE"); v == "" { + t.Fatal("AZURE_STORAGE must be set for acceptance tests") + } +} diff --git a/builtin/providers/azure/resource_azure_data_disk.go b/builtin/providers/azure/resource_azure_data_disk.go new file mode 100644 index 000000000..39316a49c --- /dev/null +++ b/builtin/providers/azure/resource_azure_data_disk.go @@ -0,0 +1,341 @@ +package azure + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/svanharmelen/azure-sdk-for-go/management" + "github.com/svanharmelen/azure-sdk-for-go/management/virtualmachinedisk" +) + +const dataDiskBlobStorageURL = "http://%s.blob.core.windows.net/disks/%s.vhd" + +func resourceAzureDataDisk() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureDataDiskCreate, + Read: resourceAzureDataDiskRead, + Update: resourceAzureDataDiskUpdate, + Delete: resourceAzureDataDiskDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "label": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "lun": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + + "size": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + + "caching": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "None", + }, + + "storage": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "media_link": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "source_media_link": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "virtual_machine": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func resourceAzureDataDiskCreate(d *schema.ResourceData, meta interface{}) error { + mc := meta.(*Client).mgmtClient + + if err := verifyDataDiskParameters(d); err != nil { + return err + } + + lun := d.Get("lun").(int) + vm := d.Get("virtual_machine").(string) + + label := d.Get("label").(string) + if label == "" { + label = fmt.Sprintf("%s-%d", vm, lun) + } + + p := virtualmachinedisk.CreateDataDiskParameters{ + DiskLabel: label, + Lun: lun, + LogicalDiskSizeInGB: d.Get("size").(int), + HostCaching: hostCaching(d), + MediaLink: mediaLink(d), + SourceMediaLink: d.Get("source_media_link").(string), + } + + if name, ok := d.GetOk("name"); ok { + p.DiskName = name.(string) + } + + log.Printf("[DEBUG] Adding data disk %d to instance: %s", lun, vm) + req, err := virtualmachinedisk.NewClient(mc).AddDataDisk(vm, vm, vm, p) + if err != nil { + return fmt.Errorf("Error adding data disk %d to instance %s: %s", lun, vm, err) + } + + // Wait until the data disk is added + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for data disk %d to be added to instance %s: %s", lun, vm, err) + } + + log.Printf("[DEBUG] Retrieving data disk %d from instance %s", lun, vm) + disk, err := virtualmachinedisk.NewClient(mc).GetDataDisk(vm, vm, vm, lun) + if err != nil { + return fmt.Errorf("Error retrieving data disk %d from instance %s: %s", lun, vm, err) + } + + d.SetId(disk.DiskName) + + return resourceAzureDataDiskRead(d, meta) +} + +func resourceAzureDataDiskRead(d *schema.ResourceData, meta interface{}) error { + mc := meta.(*Client).mgmtClient + + lun := d.Get("lun").(int) + vm := d.Get("virtual_machine").(string) + + log.Printf("[DEBUG] Retrieving data disk: %s", d.Id()) + datadisk, err := virtualmachinedisk.NewClient(mc).GetDataDisk(vm, vm, vm, lun) + if err != nil { + if management.IsResourceNotFoundError(err) { + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving data disk %s: %s", d.Id(), err) + } + + d.Set("name", datadisk.DiskName) + d.Set("label", datadisk.DiskLabel) + d.Set("lun", datadisk.Lun) + d.Set("size", datadisk.LogicalDiskSizeInGB) + d.Set("caching", datadisk.HostCaching) + d.Set("media_link", datadisk.MediaLink) + + log.Printf("[DEBUG] Retrieving disk: %s", d.Id()) + disk, err := virtualmachinedisk.NewClient(mc).GetDisk(d.Id()) + if err != nil { + return fmt.Errorf("Error retrieving disk %s: %s", d.Id(), err) + } + + d.Set("virtual_machine", disk.AttachedTo.RoleName) + + return nil +} + +func resourceAzureDataDiskUpdate(d *schema.ResourceData, meta interface{}) error { + mc := meta.(*Client).mgmtClient + diskClient := virtualmachinedisk.NewClient(mc) + + lun := d.Get("lun").(int) + vm := d.Get("virtual_machine").(string) + + if d.HasChange("lun") || d.HasChange("size") || d.HasChange("virtual_machine") { + olun, _ := d.GetChange("lun") + ovm, _ := d.GetChange("virtual_machine") + + log.Printf("[DEBUG] Detaching data disk: %s", d.Id()) + req, err := diskClient. + DeleteDataDisk(ovm.(string), ovm.(string), ovm.(string), olun.(int), false) + if err != nil { + return fmt.Errorf("Error detaching data disk %s: %s", d.Id(), err) + } + + // Wait until the data disk is detached + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for data disk %s to be detached: %s", d.Id(), err) + } + + log.Printf("[DEBUG] Verifying data disk %s is properly detached...", d.Id()) + for i := 0; i < 6; i++ { + disk, err := diskClient.GetDisk(d.Id()) + if err != nil { + return fmt.Errorf("Error retrieving disk %s: %s", d.Id(), err) + } + + // Check if the disk is really detached + if disk.AttachedTo.RoleName == "" { + break + } + + // If not, wait 30 seconds and try it again... + time.Sleep(time.Duration(30 * time.Second)) + } + + if d.HasChange("size") { + p := virtualmachinedisk.UpdateDiskParameters{ + Name: d.Id(), + Label: d.Get("label").(string), + ResizedSizeInGB: d.Get("size").(int), + } + + log.Printf("[DEBUG] Updating disk: %s", d.Id()) + req, err := diskClient.UpdateDisk(d.Id(), p) + if err != nil { + return fmt.Errorf("Error updating disk %s: %s", d.Id(), err) + } + + // Wait until the disk is updated + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for disk %s to be updated: %s", d.Id(), err) + } + } + + p := virtualmachinedisk.CreateDataDiskParameters{ + DiskName: d.Id(), + Lun: lun, + HostCaching: hostCaching(d), + MediaLink: mediaLink(d), + } + + log.Printf("[DEBUG] Attaching data disk: %s", d.Id()) + req, err = diskClient.AddDataDisk(vm, vm, vm, p) + if err != nil { + return fmt.Errorf("Error attaching data disk %s to instance %s: %s", d.Id(), vm, err) + } + + // Wait until the data disk is attached + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for data disk %s to be attached to instance %s: %s", d.Id(), vm, err) + } + + // Make sure we return here since all possible changes are + // already updated if we reach this point + return nil + } + + if d.HasChange("caching") { + p := virtualmachinedisk.UpdateDataDiskParameters{ + DiskName: d.Id(), + Lun: lun, + HostCaching: hostCaching(d), + MediaLink: mediaLink(d), + } + + log.Printf("[DEBUG] Updating data disk: %s", d.Id()) + req, err := diskClient.UpdateDataDisk(vm, vm, vm, lun, p) + if err != nil { + return fmt.Errorf("Error updating data disk %s: %s", d.Id(), err) + } + + // Wait until the data disk is updated + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for data disk %s to be updated: %s", d.Id(), err) + } + } + + return resourceAzureDataDiskRead(d, meta) +} + +func resourceAzureDataDiskDelete(d *schema.ResourceData, meta interface{}) error { + mc := meta.(*Client).mgmtClient + + lun := d.Get("lun").(int) + vm := d.Get("virtual_machine").(string) + + // If a name was not supplied, it means we created a new emtpy disk and we now want to + // delete that disk again. Otherwise we only want to detach the disk and keep the blob. + _, removeBlob := d.GetOk("name") + + log.Printf("[DEBUG] Detaching data disk %s with removeBlob = %t", d.Id(), removeBlob) + req, err := virtualmachinedisk.NewClient(mc).DeleteDataDisk(vm, vm, vm, lun, removeBlob) + if err != nil { + return fmt.Errorf( + "Error detaching data disk %s with removeBlob = %t: %s", d.Id(), removeBlob, err) + } + + // Wait until the data disk is detached and optionally deleted + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for data disk %s to be detached with removeBlob = %t: %s", + d.Id(), removeBlob, err) + } + + d.SetId("") + + return nil +} + +func hostCaching(d *schema.ResourceData) virtualmachinedisk.HostCachingType { + switch d.Get("caching").(string) { + case "ReadOnly": + return virtualmachinedisk.HostCachingTypeReadOnly + case "ReadWrite": + return virtualmachinedisk.HostCachingTypeReadWrite + default: + return virtualmachinedisk.HostCachingTypeNone + } +} + +func mediaLink(d *schema.ResourceData) string { + mediaLink, ok := d.GetOk("media_link") + if ok { + return mediaLink.(string) + } + + name, ok := d.GetOk("name") + if !ok { + name = fmt.Sprintf("%s-%d", d.Get("virtual_machine").(string), d.Get("lun").(int)) + } + + return fmt.Sprintf(dataDiskBlobStorageURL, d.Get("storage").(string), name.(string)) +} + +func verifyDataDiskParameters(d *schema.ResourceData) error { + caching := d.Get("caching").(string) + if caching != "None" && caching != "ReadOnly" && caching != "ReadWrite" { + return fmt.Errorf( + "Invalid caching type %s! Valid options are 'None', 'ReadOnly' and 'ReadWrite'.", caching) + } + + if _, ok := d.GetOk("media_link"); !ok { + if _, ok := d.GetOk("storage"); !ok { + return fmt.Errorf("If not supplying 'media_link', you must supply 'storage'.") + } + } + + return nil +} diff --git a/builtin/providers/azure/resource_azure_data_disk_test.go b/builtin/providers/azure/resource_azure_data_disk_test.go new file mode 100644 index 000000000..d2582876c --- /dev/null +++ b/builtin/providers/azure/resource_azure_data_disk_test.go @@ -0,0 +1,236 @@ +package azure + +import ( + "fmt" + "os" + "strconv" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/svanharmelen/azure-sdk-for-go/management" + "github.com/svanharmelen/azure-sdk-for-go/management/virtualmachinedisk" +) + +func TestAccAzureDataDisk_basic(t *testing.T) { + var disk virtualmachinedisk.DataDiskResponse + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureDataDiskDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureDataDisk_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureDataDiskExists( + "azure_data_disk.foo", &disk), + testAccCheckAzureDataDiskAttributes(&disk), + resource.TestCheckResourceAttr( + "azure_data_disk.foo", "label", "terraform-test-0"), + resource.TestCheckResourceAttr( + "azure_data_disk.foo", "size", "10"), + ), + }, + }, + }) +} + +func TestAccAzureDataDisk_update(t *testing.T) { + var disk virtualmachinedisk.DataDiskResponse + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureDataDiskDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureDataDisk_advanced, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureDataDiskExists( + "azure_data_disk.foo", &disk), + resource.TestCheckResourceAttr( + "azure_data_disk.foo", "label", "terraform-test1-1"), + resource.TestCheckResourceAttr( + "azure_data_disk.foo", "lun", "1"), + resource.TestCheckResourceAttr( + "azure_data_disk.foo", "size", "10"), + resource.TestCheckResourceAttr( + "azure_data_disk.foo", "caching", "ReadOnly"), + resource.TestCheckResourceAttr( + "azure_data_disk.foo", "virtual_machine", "terraform-test1"), + ), + }, + + resource.TestStep{ + Config: testAccAzureDataDisk_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureDataDiskExists( + "azure_data_disk.foo", &disk), + resource.TestCheckResourceAttr( + "azure_data_disk.foo", "label", "terraform-test1-1"), + resource.TestCheckResourceAttr( + "azure_data_disk.foo", "lun", "2"), + resource.TestCheckResourceAttr( + "azure_data_disk.foo", "size", "20"), + resource.TestCheckResourceAttr( + "azure_data_disk.foo", "caching", "ReadWrite"), + resource.TestCheckResourceAttr( + "azure_data_disk.foo", "virtual_machine", "terraform-test2"), + ), + }, + }, + }) +} + +func testAccCheckAzureDataDiskExists( + n string, + disk *virtualmachinedisk.DataDiskResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Data Disk ID is set") + } + + vm := rs.Primary.Attributes["virtual_machine"] + lun, err := strconv.Atoi(rs.Primary.Attributes["lun"]) + if err != nil { + return err + } + + mc := testAccProvider.Meta().(*Client).mgmtClient + d, err := virtualmachinedisk.NewClient(mc).GetDataDisk(vm, vm, vm, lun) + if err != nil { + return err + } + + if d.DiskName != rs.Primary.ID { + return fmt.Errorf("Data Disk not found") + } + + *disk = d + + return nil + } +} + +func testAccCheckAzureDataDiskAttributes( + disk *virtualmachinedisk.DataDiskResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if disk.Lun != 0 { + return fmt.Errorf("Bad lun: %d", disk.Lun) + } + + if disk.LogicalDiskSizeInGB != 10 { + return fmt.Errorf("Bad size: %d", disk.LogicalDiskSizeInGB) + } + + if disk.HostCaching != "None" { + return fmt.Errorf("Bad caching: %s", disk.HostCaching) + } + + return nil + } +} + +func testAccCheckAzureDataDiskDestroy(s *terraform.State) error { + mc := testAccProvider.Meta().(*Client).mgmtClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azure_data_disk" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Disk ID is set") + } + + vm := rs.Primary.Attributes["virtual_machine"] + lun, err := strconv.Atoi(rs.Primary.Attributes["lun"]) + if err != nil { + return err + } + + _, err = virtualmachinedisk.NewClient(mc).GetDataDisk(vm, vm, vm, lun) + if err == nil { + return fmt.Errorf("Resource %s still exists", rs.Primary.ID) + } + + if !management.IsResourceNotFoundError(err) { + return err + } + } + + return nil +} + +var testAccAzureDataDisk_basic = fmt.Sprintf(` +resource "azure_instance" "foo" { + name = "terraform-test" + image = "Ubuntu Server 14.04 LTS" + size = "Basic_A1" + storage = "%s" + location = "West US" + username = "terraform" + password = "Pass!admin123" +} + +resource "azure_data_disk" "foo" { + lun = 0 + size = 10 + storage = "${azure_instance.foo.storage}" + virtual_machine = "${azure_instance.foo.id}" +}`, os.Getenv("AZURE_STORAGE")) + +var testAccAzureDataDisk_advanced = fmt.Sprintf(` +resource "azure_instance" "foo" { + name = "terraform-test1" + image = "Ubuntu Server 14.04 LTS" + size = "Basic_A1" + storage = "%s" + location = "West US" + username = "terraform" + password = "Pass!admin123" +} + +resource "azure_data_disk" "foo" { + lun = 1 + size = 10 + caching = "ReadOnly" + storage = "${azure_instance.foo.storage}" + virtual_machine = "${azure_instance.foo.id}" +}`, os.Getenv("AZURE_STORAGE")) + +var testAccAzureDataDisk_update = fmt.Sprintf(` +resource "azure_instance" "foo" { + name = "terraform-test1" + image = "Ubuntu Server 14.04 LTS" + size = "Basic_A1" + storage = "%s" + location = "West US" + username = "terraform" + password = "Pass!admin123" +} + +resource "azure_instance" "bar" { + name = "terraform-test2" + image = "Ubuntu Server 14.04 LTS" + size = "Basic_A1" + storage = "${azure_instance.foo.storage}" + location = "West US" + username = "terraform" + password = "Pass!admin123" +} + +resource "azure_data_disk" "foo" { + lun = 2 + size = 20 + caching = "ReadWrite" + storage = "${azure_instance.bar.storage}" + virtual_machine = "${azure_instance.bar.id}" +}`, os.Getenv("AZURE_STORAGE")) diff --git a/builtin/providers/azure/resource_azure_instance.go b/builtin/providers/azure/resource_azure_instance.go new file mode 100644 index 000000000..ad8a77a9c --- /dev/null +++ b/builtin/providers/azure/resource_azure_instance.go @@ -0,0 +1,670 @@ +package azure + +import ( + "bytes" + "encoding/base64" + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "github.com/svanharmelen/azure-sdk-for-go/management" + "github.com/svanharmelen/azure-sdk-for-go/management/hostedservice" + "github.com/svanharmelen/azure-sdk-for-go/management/osimage" + "github.com/svanharmelen/azure-sdk-for-go/management/virtualmachine" + "github.com/svanharmelen/azure-sdk-for-go/management/virtualmachineimage" + "github.com/svanharmelen/azure-sdk-for-go/management/vmutils" +) + +const ( + linux = "Linux" + windows = "Windows" + osDiskBlobStorageURL = "http://%s.blob.core.windows.net/vhds/%s.vhd" +) + +func resourceAzureInstance() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureInstanceCreate, + Read: resourceAzureInstanceRead, + Update: resourceAzureInstanceUpdate, + Delete: resourceAzureInstanceDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "image": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "size": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "subnet": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "virtual_network": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "storage": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "reverse_dns": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "automatic_updates": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + }, + + "time_zone": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "username": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "password": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "ssh_key_thumbprint": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "endpoint": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "protocol": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "tcp", + }, + + "public_port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + + "private_port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + }, + }, + Set: resourceAzureEndpointHash, + }, + + "security_group": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "ip_address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "vip_address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAzureInstanceCreate(d *schema.ResourceData, meta interface{}) (err error) { + mc := meta.(*Client).mgmtClient + + name := d.Get("name").(string) + + // Compute/set the description + description := d.Get("description").(string) + if description == "" { + description = name + } + + // Retrieve the needed details of the image + configureForImage, osType, err := retrieveImageDetails( + mc, + d.Get("image").(string), + name, + d.Get("storage").(string), + ) + if err != nil { + return err + } + + // Verify if we have all required parameters + if err := verifyInstanceParameters(d, osType); err != nil { + return err + } + + p := hostedservice.CreateHostedServiceParameters{ + ServiceName: name, + Label: base64.StdEncoding.EncodeToString([]byte(name)), + Description: fmt.Sprintf("Cloud Service created automatically for instance %s", name), + Location: d.Get("location").(string), + ReverseDNSFqdn: d.Get("reverse_dns").(string), + } + + log.Printf("[DEBUG] Creating Cloud Service for instance: %s", name) + err = hostedservice.NewClient(mc).CreateHostedService(p) + if err != nil { + return fmt.Errorf("Error creating Cloud Service for instance %s: %s", name, err) + } + + // Put in this defer here, so we are sure to cleanup already created parts + // when we exit with an error + defer func(mc management.Client) { + if err != nil { + req, err := hostedservice.NewClient(mc).DeleteHostedService(name, true) + if err != nil { + log.Printf("[DEBUG] Error cleaning up Cloud Service of instance %s: %s", name, err) + } + + // Wait until the Cloud Service is deleted + if err := mc.WaitForOperation(req, nil); err != nil { + log.Printf( + "[DEBUG] Error waiting for Cloud Service of instance %s to be deleted: %s", name, err) + } + } + }(mc) + + // Create a new role for the instance + role := vmutils.NewVMConfiguration(name, d.Get("size").(string)) + + log.Printf("[DEBUG] Configuring deployment from image...") + err = configureForImage(&role) + if err != nil { + return fmt.Errorf("Error configuring the deployment for %s: %s", name, err) + } + + if osType == linux { + // This is pretty ugly, but the Azure SDK leaves me no other choice... + if tp, ok := d.GetOk("ssh_key_thumbprint"); ok { + err = vmutils.ConfigureForLinux( + &role, + name, + d.Get("username").(string), + d.Get("password").(string), + tp.(string), + ) + } else { + err = vmutils.ConfigureForLinux( + &role, + name, + d.Get("username").(string), + d.Get("password").(string), + ) + } + if err != nil { + return fmt.Errorf("Error configuring %s for Linux: %s", name, err) + } + } + + if osType == windows { + err = vmutils.ConfigureForWindows( + &role, + name, + d.Get("username").(string), + d.Get("password").(string), + d.Get("automatic_updates").(bool), + d.Get("time_zone").(string), + ) + if err != nil { + return fmt.Errorf("Error configuring %s for Windows: %s", name, err) + } + } + + if s := d.Get("endpoint").(*schema.Set); s.Len() > 0 { + for _, v := range s.List() { + m := v.(map[string]interface{}) + err := vmutils.ConfigureWithExternalPort( + &role, + m["name"].(string), + m["private_port"].(int), + m["public_port"].(int), + endpointProtocol(m["protocol"].(string)), + ) + if err != nil { + return fmt.Errorf( + "Error adding endpoint %s for instance %s: %s", m["name"].(string), name, err) + } + } + } + + if subnet, ok := d.GetOk("subnet"); ok { + err = vmutils.ConfigureWithSubnet(&role, subnet.(string)) + if err != nil { + return fmt.Errorf( + "Error associating subnet %s with instance %s: %s", d.Get("subnet").(string), name, err) + } + } + + if sg, ok := d.GetOk("security_group"); ok { + err = vmutils.ConfigureWithSecurityGroup(&role, sg.(string)) + if err != nil { + return fmt.Errorf( + "Error associating security group %s with instance %s: %s", sg.(string), name, err) + } + } + + options := virtualmachine.CreateDeploymentOptions{ + VirtualNetworkName: d.Get("virtual_network").(string), + } + + log.Printf("[DEBUG] Creating the new instance...") + req, err := virtualmachine.NewClient(mc).CreateDeployment(role, name, options) + if err != nil { + return fmt.Errorf("Error creating instance %s: %s", name, err) + } + + log.Printf("[DEBUG] Waiting for the new instance to be created...") + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for instance %s to be created: %s", name, err) + } + + d.SetId(name) + + return resourceAzureInstanceRead(d, meta) +} + +func resourceAzureInstanceRead(d *schema.ResourceData, meta interface{}) error { + mc := meta.(*Client).mgmtClient + + log.Printf("[DEBUG] Retrieving Cloud Service for instance: %s", d.Id()) + cs, err := hostedservice.NewClient(mc).GetHostedService(d.Id()) + if err != nil { + return fmt.Errorf("Error retrieving Cloud Service of instance %s: %s", d.Id(), err) + } + + d.Set("reverse_dns", cs.ReverseDNSFqdn) + d.Set("location", cs.Location) + + log.Printf("[DEBUG] Retrieving instance: %s", d.Id()) + dpmt, err := virtualmachine.NewClient(mc).GetDeployment(d.Id(), d.Id()) + if err != nil { + if management.IsResourceNotFoundError(err) { + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving instance %s: %s", d.Id(), err) + } + + if len(dpmt.RoleList) != 1 { + return fmt.Errorf( + "Instance %s has an unexpected number of roles: %d", d.Id(), len(dpmt.RoleList)) + } + + d.Set("size", dpmt.RoleList[0].RoleSize) + + if len(dpmt.RoleInstanceList) != 1 { + return fmt.Errorf( + "Instance %s has an unexpected number of role instances: %d", + d.Id(), len(dpmt.RoleInstanceList)) + } + d.Set("ip_address", dpmt.RoleInstanceList[0].IPAddress) + + if len(dpmt.RoleInstanceList[0].InstanceEndpoints) > 0 { + d.Set("vip_address", dpmt.RoleInstanceList[0].InstanceEndpoints[0].Vip) + } + + // Find the network configuration set + for _, c := range dpmt.RoleList[0].ConfigurationSets { + if c.ConfigurationSetType == virtualmachine.ConfigurationSetTypeNetwork { + // Create a new set to hold all configured endpoints + endpoints := &schema.Set{ + F: resourceAzureEndpointHash, + } + + // Loop through all endpoints + for _, ep := range c.InputEndpoints { + endpoint := map[string]interface{}{} + + // Update the values + endpoint["name"] = ep.Name + endpoint["protocol"] = string(ep.Protocol) + endpoint["public_port"] = ep.Port + endpoint["private_port"] = ep.LocalPort + endpoints.Add(endpoint) + } + d.Set("endpoint", endpoints) + + // Update the subnet + switch len(c.SubnetNames) { + case 1: + d.Set("subnet", c.SubnetNames[0]) + case 0: + d.Set("subnet", "") + default: + return fmt.Errorf( + "Instance %s has an unexpected number of associated subnets %d", + d.Id(), len(dpmt.RoleInstanceList)) + } + + // Update the security group + d.Set("security_group", c.NetworkSecurityGroup) + } + } + + connType := "ssh" + if dpmt.RoleList[0].OSVirtualHardDisk.OS == windows { + connType = "winrm" + } + + // Set the connection info for any configured provisioners + d.SetConnInfo(map[string]string{ + "type": connType, + "host": dpmt.VirtualIPs[0].Address, + "user": d.Get("username").(string), + "password": d.Get("password").(string), + }) + + return nil +} + +func resourceAzureInstanceUpdate(d *schema.ResourceData, meta interface{}) error { + mc := meta.(*Client).mgmtClient + + // First check if anything we can update changed, and if not just return + if !d.HasChange("size") && !d.HasChange("endpoint") && !d.HasChange("security_group") { + return nil + } + + // Get the current role + role, err := virtualmachine.NewClient(mc).GetRole(d.Id(), d.Id(), d.Id()) + if err != nil { + return fmt.Errorf("Error retrieving role of instance %s: %s", d.Id(), err) + } + + // Verify if we have all required parameters + if err := verifyInstanceParameters(d, role.OSVirtualHardDisk.OS); err != nil { + return err + } + + if d.HasChange("size") { + role.RoleSize = d.Get("size").(string) + } + + if d.HasChange("endpoint") { + _, n := d.GetChange("endpoint") + + // Delete the existing endpoints + for i, c := range role.ConfigurationSets { + if c.ConfigurationSetType == virtualmachine.ConfigurationSetTypeNetwork { + c.InputEndpoints = nil + role.ConfigurationSets[i] = c + } + } + + // And add the ones we still want + if s := n.(*schema.Set); s.Len() > 0 { + for _, v := range s.List() { + m := v.(map[string]interface{}) + err := vmutils.ConfigureWithExternalPort( + role, + m["name"].(string), + m["private_port"].(int), + m["public_port"].(int), + endpointProtocol(m["protocol"].(string)), + ) + if err != nil { + return fmt.Errorf( + "Error adding endpoint %s for instance %s: %s", m["name"].(string), d.Id(), err) + } + } + } + } + + if d.HasChange("security_group") { + sg := d.Get("security_group").(string) + err := vmutils.ConfigureWithSecurityGroup(role, sg) + if err != nil { + return fmt.Errorf( + "Error associating security group %s with instance %s: %s", sg, d.Id(), err) + } + } + + // Update the adjusted role + req, err := virtualmachine.NewClient(mc).UpdateRole(d.Id(), d.Id(), d.Id(), *role) + if err != nil { + return fmt.Errorf("Error updating role of instance %s: %s", d.Id(), err) + } + + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for role of instance %s to be updated: %s", d.Id(), err) + } + + return resourceAzureInstanceRead(d, meta) +} + +func resourceAzureInstanceDelete(d *schema.ResourceData, meta interface{}) error { + mc := meta.(*Client).mgmtClient + + log.Printf("[DEBUG] Deleting instance: %s", d.Id()) + req, err := hostedservice.NewClient(mc).DeleteHostedService(d.Id(), true) + if err != nil { + return fmt.Errorf("Error deleting instance %s: %s", d.Id(), err) + } + + // Wait until the instance is deleted + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for instance %s to be deleted: %s", d.Id(), err) + } + + d.SetId("") + + return nil +} + +func resourceAzureEndpointHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string))) + buf.WriteString(fmt.Sprintf("%d-", m["public_port"].(int))) + buf.WriteString(fmt.Sprintf("%d-", m["private_port"].(int))) + + return hashcode.String(buf.String()) +} + +func retrieveImageDetails( + mc management.Client, + label string, + name string, + storage string) (func(*virtualmachine.Role) error, string, error) { + configureForImage, osType, VMLabels, err := retrieveVMImageDetails(mc, label) + if err == nil { + return configureForImage, osType, nil + } + + configureForImage, osType, OSLabels, err := retrieveOSImageDetails(mc, label, name, storage) + if err == nil { + return configureForImage, osType, nil + } + + return nil, "", fmt.Errorf("Could not find image with label '%s'. Available images are: %s", + label, strings.Join(append(VMLabels, OSLabels...), ", ")) +} + +func retrieveVMImageDetails( + mc management.Client, + label string) (func(*virtualmachine.Role) error, string, []string, error) { + imgs, err := virtualmachineimage.NewClient(mc).ListVirtualMachineImages() + if err != nil { + return nil, "", nil, fmt.Errorf("Error retrieving image details: %s", err) + } + + var labels []string + for _, img := range imgs.VMImages { + if img.Label == label { + if img.OSDiskConfiguration.OS != linux && img.OSDiskConfiguration.OS != windows { + return nil, "", nil, fmt.Errorf("Unsupported image OS: %s", img.OSDiskConfiguration.OS) + } + + configureForImage := func(role *virtualmachine.Role) error { + return vmutils.ConfigureDeploymentFromVMImage( + role, + img.Name, + "", + true, + ) + } + + return configureForImage, img.OSDiskConfiguration.OS, nil, nil + } + + labels = append(labels, img.Label) + } + + return nil, "", labels, fmt.Errorf("Could not find image with label '%s'", label) +} + +func retrieveOSImageDetails( + mc management.Client, + label string, + name string, + storage string) (func(*virtualmachine.Role) error, string, []string, error) { + imgs, err := osimage.NewClient(mc).ListOSImages() + if err != nil { + return nil, "", nil, fmt.Errorf("Error retrieving image details: %s", err) + } + + var labels []string + for _, img := range imgs.OSImages { + if img.Label == label { + if img.OS != linux && img.OS != windows { + return nil, "", nil, fmt.Errorf("Unsupported image OS: %s", img.OS) + } + if img.MediaLink == "" { + if storage == "" { + return nil, "", nil, + fmt.Errorf("When using a platform image, the 'storage' parameter is required") + } + img.MediaLink = fmt.Sprintf(osDiskBlobStorageURL, storage, name) + } + + configureForImage := func(role *virtualmachine.Role) error { + return vmutils.ConfigureDeploymentFromPlatformImage( + role, + img.Name, + img.MediaLink, + label, + ) + } + + return configureForImage, img.OS, nil, nil + } + + labels = append(labels, img.Label) + } + + return nil, "", labels, fmt.Errorf("Could not find image with label '%s'", label) +} + +func endpointProtocol(p string) virtualmachine.InputEndpointProtocol { + if p == "tcp" { + return virtualmachine.InputEndpointProtocolTCP + } + + return virtualmachine.InputEndpointProtocolUDP +} + +func verifyInstanceParameters(d *schema.ResourceData, osType string) error { + if osType == linux { + _, pass := d.GetOk("password") + _, key := d.GetOk("ssh_key_thumbprint") + + if !pass && !key { + return fmt.Errorf( + "You must supply a 'password' and/or a 'ssh_key_thumbprint' when using a Linux image") + } + } + + if osType == windows { + if _, ok := d.GetOk("password"); !ok { + return fmt.Errorf("You must supply a 'password' when using a Windows image") + } + + if _, ok := d.GetOk("time_zone"); !ok { + return fmt.Errorf("You must supply a 'time_zone' when using a Windows image") + } + } + + if _, ok := d.GetOk("subnet"); ok { + if _, ok := d.GetOk("virtual_network"); !ok { + return fmt.Errorf("You must also supply a 'virtual_network' when supplying a 'subnet'") + } + } + + if s := d.Get("endpoint").(*schema.Set); s.Len() > 0 { + for _, v := range s.List() { + protocol := v.(map[string]interface{})["protocol"].(string) + + if protocol != "tcp" && protocol != "udp" { + return fmt.Errorf( + "Invalid endpoint protocol %s! Valid options are 'tcp' and 'udp'.", protocol) + } + } + } + + return nil +} diff --git a/builtin/providers/azure/resource_azure_instance_test.go b/builtin/providers/azure/resource_azure_instance_test.go new file mode 100644 index 000000000..0d4c9b043 --- /dev/null +++ b/builtin/providers/azure/resource_azure_instance_test.go @@ -0,0 +1,455 @@ +package azure + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/svanharmelen/azure-sdk-for-go/management" + "github.com/svanharmelen/azure-sdk-for-go/management/hostedservice" + "github.com/svanharmelen/azure-sdk-for-go/management/virtualmachine" +) + +func TestAccAzureInstance_basic(t *testing.T) { + var dpmt virtualmachine.DeploymentResponse + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureInstance_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureInstanceExists( + "azure_instance.foo", &dpmt), + testAccCheckAzureInstanceBasicAttributes(&dpmt), + resource.TestCheckResourceAttr( + "azure_instance.foo", "name", "terraform-test"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "location", "West US"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "endpoint.2462817782.public_port", "22"), + ), + }, + }, + }) +} + +func TestAccAzureInstance_advanced(t *testing.T) { + var dpmt virtualmachine.DeploymentResponse + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureInstance_advanced, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureInstanceExists( + "azure_instance.foo", &dpmt), + testAccCheckAzureInstanceAdvancedAttributes(&dpmt), + resource.TestCheckResourceAttr( + "azure_instance.foo", "name", "terraform-test1"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "size", "Basic_A1"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "subnet", "subnet1"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "virtual_network", "terraform-vnet"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "security_group", "terraform-security-group1"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "endpoint.1814039778.public_port", "3389"), + ), + }, + }, + }) +} + +func TestAccAzureInstance_update(t *testing.T) { + var dpmt virtualmachine.DeploymentResponse + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureInstance_advanced, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureInstanceExists( + "azure_instance.foo", &dpmt), + testAccCheckAzureInstanceAdvancedAttributes(&dpmt), + resource.TestCheckResourceAttr( + "azure_instance.foo", "name", "terraform-test1"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "size", "Basic_A1"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "subnet", "subnet1"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "virtual_network", "terraform-vnet"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "security_group", "terraform-security-group1"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "endpoint.1814039778.public_port", "3389"), + ), + }, + + resource.TestStep{ + Config: testAccAzureInstance_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureInstanceExists( + "azure_instance.foo", &dpmt), + testAccCheckAzureInstanceUpdatedAttributes(&dpmt), + resource.TestCheckResourceAttr( + "azure_instance.foo", "size", "Basic_A2"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "security_group", "terraform-security-group2"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "endpoint.1814039778.public_port", "3389"), + resource.TestCheckResourceAttr( + "azure_instance.foo", "endpoint.3713350066.public_port", "5985"), + ), + }, + }, + }) +} + +func testAccCheckAzureInstanceExists( + n string, + dpmt *virtualmachine.DeploymentResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + mc := testAccProvider.Meta().(*Client).mgmtClient + vm, err := virtualmachine.NewClient(mc).GetDeployment(rs.Primary.ID, rs.Primary.ID) + if err != nil { + return err + } + + if vm.Name != rs.Primary.ID { + return fmt.Errorf("Instance not found") + } + + *dpmt = vm + + return nil + } +} + +func testAccCheckAzureInstanceBasicAttributes( + dpmt *virtualmachine.DeploymentResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if dpmt.Name != "terraform-test" { + return fmt.Errorf("Bad name: %s", dpmt.Name) + } + + if len(dpmt.RoleList) != 1 { + return fmt.Errorf( + "Instance %s has an unexpected number of roles: %d", dpmt.Name, len(dpmt.RoleList)) + } + + if dpmt.RoleList[0].RoleSize != "Basic_A1" { + return fmt.Errorf("Bad size: %s", dpmt.RoleList[0].RoleSize) + } + + return nil + } +} + +func testAccCheckAzureInstanceAdvancedAttributes( + dpmt *virtualmachine.DeploymentResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if dpmt.Name != "terraform-test1" { + return fmt.Errorf("Bad name: %s", dpmt.Name) + } + + if dpmt.VirtualNetworkName != "terraform-vnet" { + return fmt.Errorf("Bad virtual network: %s", dpmt.VirtualNetworkName) + } + + if len(dpmt.RoleList) != 1 { + return fmt.Errorf( + "Instance %s has an unexpected number of roles: %d", dpmt.Name, len(dpmt.RoleList)) + } + + if dpmt.RoleList[0].RoleSize != "Basic_A1" { + return fmt.Errorf("Bad size: %s", dpmt.RoleList[0].RoleSize) + } + + for _, c := range dpmt.RoleList[0].ConfigurationSets { + if c.ConfigurationSetType == virtualmachine.ConfigurationSetTypeNetwork { + if len(c.InputEndpoints) != 1 { + return fmt.Errorf( + "Instance %s has an unexpected number of endpoints %d", + dpmt.Name, len(c.InputEndpoints)) + } + + if c.InputEndpoints[0].Name != "RDP" { + return fmt.Errorf("Bad endpoint name: %s", c.InputEndpoints[0].Name) + } + + if c.InputEndpoints[0].Port != 3389 { + return fmt.Errorf("Bad endpoint port: %d", c.InputEndpoints[0].Port) + } + + if len(c.SubnetNames) != 1 { + return fmt.Errorf( + "Instance %s has an unexpected number of associated subnets %d", + dpmt.Name, len(c.SubnetNames)) + } + + if c.SubnetNames[0] != "subnet1" { + return fmt.Errorf("Bad subnet: %s", c.SubnetNames[0]) + } + + if c.NetworkSecurityGroup != "terraform-security-group1" { + return fmt.Errorf("Bad security group: %s", c.NetworkSecurityGroup) + } + } + } + + return nil + } +} + +func testAccCheckAzureInstanceUpdatedAttributes( + dpmt *virtualmachine.DeploymentResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if dpmt.Name != "terraform-test1" { + return fmt.Errorf("Bad name: %s", dpmt.Name) + } + + if dpmt.VirtualNetworkName != "terraform-vnet" { + return fmt.Errorf("Bad virtual network: %s", dpmt.VirtualNetworkName) + } + + if len(dpmt.RoleList) != 1 { + return fmt.Errorf( + "Instance %s has an unexpected number of roles: %d", dpmt.Name, len(dpmt.RoleList)) + } + + if dpmt.RoleList[0].RoleSize != "Basic_A2" { + return fmt.Errorf("Bad size: %s", dpmt.RoleList[0].RoleSize) + } + + for _, c := range dpmt.RoleList[0].ConfigurationSets { + if c.ConfigurationSetType == virtualmachine.ConfigurationSetTypeNetwork { + if len(c.InputEndpoints) != 2 { + return fmt.Errorf( + "Instance %s has an unexpected number of endpoints %d", + dpmt.Name, len(c.InputEndpoints)) + } + + if c.InputEndpoints[1].Name != "WINRM" { + return fmt.Errorf("Bad endpoint name: %s", c.InputEndpoints[1].Name) + } + + if c.InputEndpoints[1].Port != 5985 { + return fmt.Errorf("Bad endpoint port: %d", c.InputEndpoints[1].Port) + } + + if len(c.SubnetNames) != 1 { + return fmt.Errorf( + "Instance %s has an unexpected number of associated subnets %d", + dpmt.Name, len(c.SubnetNames)) + } + + if c.SubnetNames[0] != "subnet1" { + return fmt.Errorf("Bad subnet: %s", c.SubnetNames[0]) + } + + if c.NetworkSecurityGroup != "terraform-security-group2" { + return fmt.Errorf("Bad security group: %s", c.NetworkSecurityGroup) + } + } + } + + return nil + } +} + +func testAccCheckAzureInstanceDestroy(s *terraform.State) error { + mc := testAccProvider.Meta().(*Client).mgmtClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azure_instance" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + _, err := hostedservice.NewClient(mc).GetHostedService(rs.Primary.ID) + if err == nil { + return fmt.Errorf("Resource %s still exists", rs.Primary.ID) + } + + if !management.IsResourceNotFoundError(err) { + return err + } + } + + return nil +} + +var testAccAzureInstance_basic = fmt.Sprintf(` +resource "azure_instance" "foo" { + name = "terraform-test" + image = "Ubuntu Server 14.04 LTS" + size = "Basic_A1" + storage = "%s" + location = "West US" + username = "terraform" + password = "Pass!admin123" + + endpoint { + name = "SSH" + protocol = "tcp" + public_port = 22 + private_port = 22 + } +}`, os.Getenv("AZURE_STORAGE")) + +var testAccAzureInstance_advanced = fmt.Sprintf(` +resource "azure_virtual_network" "foo" { + name = "terraform-vnet" + address_space = ["10.1.2.0/24"] + location = "West US" + + subnet { + name = "subnet1" + address_prefix = "10.1.2.0/25" + } + + subnet { + name = "subnet2" + address_prefix = "10.1.2.128/25" + } +} + +resource "azure_security_group" "foo" { + name = "terraform-security-group1" + location = "West US" + + rule { + name = "rdp" + priority = 101 + source_cidr = "*" + source_port = "*" + destination_cidr = "*" + destination_port = 3389 + protocol = "TCP" + } +} + +resource "azure_instance" "foo" { + name = "terraform-test1" + image = "Windows Server 2012 R2 Datacenter, April 2015" + size = "Basic_A1" + storage = "%s" + location = "West US" + time_zone = "America/Los_Angeles" + subnet = "subnet1" + virtual_network = "${azure_virtual_network.foo.name}" + security_group = "${azure_security_group.foo.name}" + username = "terraform" + password = "Pass!admin123" + + endpoint { + name = "RDP" + protocol = "tcp" + public_port = 3389 + private_port = 3389 + } +}`, os.Getenv("AZURE_STORAGE")) + +var testAccAzureInstance_update = fmt.Sprintf(` +resource "azure_virtual_network" "foo" { + name = "terraform-vnet" + address_space = ["10.1.2.0/24"] + location = "West US" + + subnet { + name = "subnet1" + address_prefix = "10.1.2.0/25" + } + + subnet { + name = "subnet2" + address_prefix = "10.1.2.128/25" + } +} + +resource "azure_security_group" "foo" { + name = "terraform-security-group1" + location = "West US" + + rule { + name = "rdp" + priority = 101 + source_cidr = "*" + source_port = "*" + destination_cidr = "*" + destination_port = 3389 + protocol = "TCP" + } +} + +resource "azure_security_group" "bar" { + name = "terraform-security-group2" + location = "West US" + + rule { + name = "rdp" + priority = 101 + source_cidr = "192.168.0.0/24" + source_port = "*" + destination_cidr = "*" + destination_port = 3389 + protocol = "TCP" + } +} + +resource "azure_instance" "foo" { + name = "terraform-test1" + image = "Windows Server 2012 R2 Datacenter, April 2015" + size = "Basic_A2" + storage = "%s" + location = "West US" + time_zone = "America/Los_Angeles" + subnet = "subnet1" + virtual_network = "${azure_virtual_network.foo.name}" + security_group = "${azure_security_group.bar.name}" + username = "terraform" + password = "Pass!admin123" + + endpoint { + name = "RDP" + protocol = "tcp" + public_port = 3389 + private_port = 3389 + } + + endpoint { + name = "WINRM" + protocol = "tcp" + public_port = 5985 + private_port = 5985 + } +}`, os.Getenv("AZURE_STORAGE")) diff --git a/builtin/providers/azure/resource_azure_security_group.go b/builtin/providers/azure/resource_azure_security_group.go new file mode 100644 index 000000000..842b12434 --- /dev/null +++ b/builtin/providers/azure/resource_azure_security_group.go @@ -0,0 +1,353 @@ +package azure + +import ( + "bytes" + "fmt" + "log" + "strconv" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "github.com/svanharmelen/azure-sdk-for-go/management" + "github.com/svanharmelen/azure-sdk-for-go/management/networksecuritygroup" +) + +func resourceAzureSecurityGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureSecurityGroupCreate, + Read: resourceAzureSecurityGroupRead, + Update: resourceAzureSecurityGroupUpdate, + Delete: resourceAzureSecurityGroupDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "label": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "rule": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "Inbound", + }, + + "priority": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + + "action": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "Allow", + }, + + "source_cidr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "source_port": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "destination_cidr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "destination_port": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "protocol": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "TCP", + }, + }, + }, + Set: resourceAzureSecurityGroupRuleHash, + }, + }, + } +} + +func resourceAzureSecurityGroupCreate(d *schema.ResourceData, meta interface{}) (err error) { + mc := meta.(*Client).mgmtClient + + name := d.Get("name").(string) + + // Compute/set the label + label := d.Get("label").(string) + if label == "" { + label = name + } + + req, err := networksecuritygroup.NewClient(mc).CreateNetworkSecurityGroup( + name, + label, + d.Get("location").(string), + ) + if err != nil { + return fmt.Errorf("Error creating Network Security Group %s: %s", name, err) + } + + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for Network Security Group %s to be created: %s", name, err) + } + + d.SetId(name) + + // Create all rules that are configured + if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 { + + // Create an empty schema.Set to hold all rules + rules := &schema.Set{ + F: resourceAzureSecurityGroupRuleHash, + } + + for _, rule := range rs.List() { + // Create a single rule + err := resourceAzureSecurityGroupRuleCreate(d, meta, rule.(map[string]interface{})) + + // We need to update this first to preserve the correct state + rules.Add(rule) + d.Set("rule", rules) + + if err != nil { + return err + } + } + } + + return resourceAzureSecurityGroupRead(d, meta) +} + +func resourceAzureSecurityGroupRuleCreate( + d *schema.ResourceData, + meta interface{}, + rule map[string]interface{}) error { + mc := meta.(*Client).mgmtClient + + // Make sure all required parameters are there + if err := verifySecurityGroupRuleParams(rule); err != nil { + return err + } + + name := rule["name"].(string) + + // Create the rule + req, err := networksecuritygroup.NewClient(mc).SetNetworkSecurityGroupRule(d.Id(), + networksecuritygroup.RuleRequest{ + Name: name, + Type: networksecuritygroup.RuleType(rule["type"].(string)), + Priority: rule["priority"].(int), + Action: networksecuritygroup.RuleAction(rule["action"].(string)), + SourceAddressPrefix: rule["source_cidr"].(string), + SourcePortRange: rule["source_port"].(string), + DestinationAddressPrefix: rule["destination_cidr"].(string), + DestinationPortRange: rule["destination_port"].(string), + Protocol: networksecuritygroup.RuleProtocol(rule["protocol"].(string)), + }, + ) + if err != nil { + return fmt.Errorf("Error creating Network Security Group rule %s: %s", name, err) + } + + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for Network Security Group rule %s to be created: %s", name, err) + } + + return nil +} + +func resourceAzureSecurityGroupRead(d *schema.ResourceData, meta interface{}) error { + mc := meta.(*Client).mgmtClient + + sg, err := networksecuritygroup.NewClient(mc).GetNetworkSecurityGroup(d.Id()) + if err != nil { + if management.IsResourceNotFoundError(err) { + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving Network Security Group %s: %s", d.Id(), err) + } + + d.Set("label", sg.Label) + d.Set("location", sg.Location) + + // Create an empty schema.Set to hold all rules + rules := &schema.Set{ + F: resourceAzureSecurityGroupRuleHash, + } + + for _, r := range sg.Rules { + if !r.IsDefault { + rule := map[string]interface{}{ + "name": r.Name, + "type": string(r.Type), + "priority": r.Priority, + "action": string(r.Action), + "source_cidr": r.SourceAddressPrefix, + "source_port": r.SourcePortRange, + "destination_cidr": r.DestinationAddressPrefix, + "destination_port": r.DestinationPortRange, + "protocol": string(r.Protocol), + } + rules.Add(rule) + } + } + + d.Set("rule", rules) + + return nil +} + +func resourceAzureSecurityGroupUpdate(d *schema.ResourceData, meta interface{}) error { + // Check if the rule set as a whole has changed + if d.HasChange("rule") { + o, n := d.GetChange("rule") + ors := o.(*schema.Set).Difference(n.(*schema.Set)) + nrs := n.(*schema.Set).Difference(o.(*schema.Set)) + + // Now first loop through all the old rules and delete any obsolete ones + for _, rule := range ors.List() { + // Delete the rule as it no longer exists in the config + err := resourceAzureSecurityGroupRuleDelete(d, meta, rule.(map[string]interface{})) + if err != nil { + return err + } + } + + // Make sure we save the state of the currently configured rules + rules := o.(*schema.Set).Intersection(n.(*schema.Set)) + d.Set("rule", rules) + + // Then loop through al the currently configured rules and create the new ones + for _, rule := range nrs.List() { + err := resourceAzureSecurityGroupRuleCreate(d, meta, rule.(map[string]interface{})) + + // We need to update this first to preserve the correct state + rules.Add(rule) + d.Set("rule", rules) + + if err != nil { + return err + } + } + } + + return resourceAzureSecurityGroupRead(d, meta) +} + +func resourceAzureSecurityGroupDelete(d *schema.ResourceData, meta interface{}) error { + mc := meta.(*Client).mgmtClient + + log.Printf("[DEBUG] Deleting Network Security Group: %s", d.Id()) + req, err := networksecuritygroup.NewClient(mc).DeleteNetworkSecurityGroup(d.Id()) + if err != nil { + return fmt.Errorf("Error deleting Network Security Group %s: %s", d.Id(), err) + } + + // Wait until the network security group is deleted + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for Network Security Group %s to be deleted: %s", d.Id(), err) + } + + d.SetId("") + + return nil +} + +func resourceAzureSecurityGroupRuleDelete( + d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error { + mc := meta.(*Client).mgmtClient + + name := rule["name"].(string) + + // Delete the rule + req, err := networksecuritygroup.NewClient(mc).DeleteNetworkSecurityGroupRule(d.Id(), name) + if err != nil { + if management.IsResourceNotFoundError(err) { + return nil + } + return fmt.Errorf("Error deleting Network Security Group rule %s: %s", name, err) + } + + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for Network Security Group rule %s to be deleted: %s", name, err) + } + + return nil +} + +func resourceAzureSecurityGroupRuleHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf( + "%s-%d-%s-%s-%s-%s-%s-%s", + m["type"].(string), + m["priority"].(int), + m["action"].(string), + m["source_cidr"].(string), + m["source_port"].(string), + m["destination_cidr"].(string), + m["destination_port"].(string), + m["protocol"].(string))) + + return hashcode.String(buf.String()) +} + +func verifySecurityGroupRuleParams(rule map[string]interface{}) error { + typ := rule["type"].(string) + if typ != "Inbound" && typ != "Outbound" { + return fmt.Errorf("Parameter type only accepts 'Inbound' or 'Outbound' as values") + } + + action := rule["action"].(string) + if action != "Allow" && action != "Deny" { + return fmt.Errorf("Parameter action only accepts 'Allow' or 'Deny' as values") + } + + protocol := rule["protocol"].(string) + if protocol != "TCP" && protocol != "UDP" && protocol != "*" { + _, err := strconv.ParseInt(protocol, 0, 0) + if err != nil { + return fmt.Errorf( + "Parameter type only accepts 'TCP', 'UDP' or '*' as values") + } + } + + return nil +} diff --git a/builtin/providers/azure/resource_azure_security_group_test.go b/builtin/providers/azure/resource_azure_security_group_test.go new file mode 100644 index 000000000..a3ce2dd53 --- /dev/null +++ b/builtin/providers/azure/resource_azure_security_group_test.go @@ -0,0 +1,271 @@ +package azure + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/svanharmelen/azure-sdk-for-go/management" + "github.com/svanharmelen/azure-sdk-for-go/management/networksecuritygroup" +) + +func TestAccAzureSecurityGroup_basic(t *testing.T) { + var group networksecuritygroup.SecurityGroupResponse + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureSecurityGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureSecurityGroup_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureSecurityGroupExists( + "azure_security_group.foo", &group), + testAccCheckAzureSecurityGroupBasicAttributes(&group), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "name", "terraform-security-group"), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "location", "West US"), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "rule.936204579.name", "RDP"), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "rule.936204579.source_port", "*"), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "rule.936204579.destination_port", "3389"), + ), + }, + }, + }) +} + +func TestAccAzureSecurityGroup_update(t *testing.T) { + var group networksecuritygroup.SecurityGroupResponse + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureSecurityGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureSecurityGroup_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureSecurityGroupExists( + "azure_security_group.foo", &group), + testAccCheckAzureSecurityGroupBasicAttributes(&group), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "name", "terraform-security-group"), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "location", "West US"), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "rule.936204579.name", "RDP"), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "rule.936204579.source_cidr", "*"), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "rule.936204579.destination_port", "3389"), + ), + }, + + resource.TestStep{ + Config: testAccAzureSecurityGroup_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureSecurityGroupExists( + "azure_security_group.foo", &group), + testAccCheckAzureSecurityGroupUpdatedAttributes(&group), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "rule.3322523298.name", "RDP"), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "rule.3322523298.source_cidr", "192.168.0.0/24"), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "rule.3322523298.destination_port", "3389"), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "rule.3929353075.name", "WINRM"), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "rule.3929353075.source_cidr", "192.168.0.0/24"), + resource.TestCheckResourceAttr( + "azure_security_group.foo", "rule.3929353075.destination_port", "5985"), + ), + }, + }, + }) +} + +func testAccCheckAzureSecurityGroupExists( + n string, + group *networksecuritygroup.SecurityGroupResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Network Security Group ID is set") + } + + mc := testAccProvider.Meta().(*Client).mgmtClient + sg, err := networksecuritygroup.NewClient(mc).GetNetworkSecurityGroup(rs.Primary.ID) + if err != nil { + return err + } + + if sg.Name != rs.Primary.ID { + return fmt.Errorf("Security Group not found") + } + + *group = sg + + return nil + } +} + +func testAccCheckAzureSecurityGroupBasicAttributes( + group *networksecuritygroup.SecurityGroupResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if group.Name != "terraform-security-group" { + return fmt.Errorf("Bad name: %s", group.Name) + } + + for _, r := range group.Rules { + if !r.IsDefault { + if r.Name != "RDP" { + return fmt.Errorf("Bad rule name: %s", r.Name) + } + if r.Priority != 101 { + return fmt.Errorf("Bad rule priority: %d", r.Priority) + } + if r.SourceAddressPrefix != "*" { + return fmt.Errorf("Bad source CIDR: %s", r.SourceAddressPrefix) + } + if r.DestinationAddressPrefix != "*" { + return fmt.Errorf("Bad destination CIDR: %s", r.DestinationAddressPrefix) + } + if r.DestinationPortRange != "3389" { + return fmt.Errorf("Bad destination port: %s", r.DestinationPortRange) + } + } + } + + return nil + } +} + +func testAccCheckAzureSecurityGroupUpdatedAttributes( + group *networksecuritygroup.SecurityGroupResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if group.Name != "terraform-security-group" { + return fmt.Errorf("Bad name: %s", group.Name) + } + + foundRDP := false + foundWINRM := false + for _, r := range group.Rules { + if !r.IsDefault { + if r.Name == "RDP" { + if r.SourceAddressPrefix != "192.168.0.0/24" { + return fmt.Errorf("Bad source CIDR: %s", r.SourceAddressPrefix) + } + + foundRDP = true + } + + if r.Name == "WINRM" { + if r.Priority != 102 { + return fmt.Errorf("Bad rule priority: %d", r.Priority) + } + if r.SourceAddressPrefix != "192.168.0.0/24" { + return fmt.Errorf("Bad source CIDR: %s", r.SourceAddressPrefix) + } + if r.DestinationAddressPrefix != "*" { + return fmt.Errorf("Bad destination CIDR: %s", r.DestinationAddressPrefix) + } + if r.DestinationPortRange != "5985" { + return fmt.Errorf("Bad destination port: %s", r.DestinationPortRange) + } + + foundWINRM = true + } + } + } + + if !foundRDP { + return fmt.Errorf("RDP rule not found") + } + + if !foundWINRM { + return fmt.Errorf("WINRM rule not found") + } + + return nil + } +} + +func testAccCheckAzureSecurityGroupDestroy(s *terraform.State) error { + mc := testAccProvider.Meta().(*Client).mgmtClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azure_security_group" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Network Security Group ID is set") + } + + _, err := networksecuritygroup.NewClient(mc).GetNetworkSecurityGroup(rs.Primary.ID) + if err == nil { + return fmt.Errorf("Resource %s still exists", rs.Primary.ID) + } + + if !management.IsResourceNotFoundError(err) { + return err + } + } + + return nil +} + +const testAccAzureSecurityGroup_basic = ` +resource "azure_security_group" "foo" { + name = "terraform-security-group" + location = "West US" + + rule { + name = "RDP" + priority = 101 + source_cidr = "*" + source_port = "*" + destination_cidr = "*" + destination_port = "3389" + protocol = "TCP" + } +}` + +const testAccAzureSecurityGroup_update = ` +resource "azure_security_group" "foo" { + name = "terraform-security-group" + location = "West US" + + rule { + name = "RDP" + priority = 101 + source_cidr = "192.168.0.0/24" + source_port = "*" + destination_cidr = "*" + destination_port = "3389" + protocol = "TCP" + } + + rule { + name = "WINRM" + priority = 102 + source_cidr = "192.168.0.0/24" + source_port = "*" + destination_cidr = "*" + destination_port = "5985" + protocol = "TCP" + } +}` diff --git a/builtin/providers/azure/resource_azure_virtual_network.go b/builtin/providers/azure/resource_azure_virtual_network.go new file mode 100644 index 000000000..be66c1a42 --- /dev/null +++ b/builtin/providers/azure/resource_azure_virtual_network.go @@ -0,0 +1,357 @@ +package azure + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "github.com/mitchellh/mapstructure" + "github.com/svanharmelen/azure-sdk-for-go/management" + "github.com/svanharmelen/azure-sdk-for-go/management/networksecuritygroup" + "github.com/svanharmelen/azure-sdk-for-go/management/virtualnetwork" +) + +const ( + virtualNetworkRetrievalError = "Error retrieving Virtual Network Configuration: %s" +) + +func resourceAzureVirtualNetwork() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureVirtualNetworkCreate, + Read: resourceAzureVirtualNetworkRead, + Update: resourceAzureVirtualNetworkUpdate, + Delete: resourceAzureVirtualNetworkDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "address_space": &schema.Schema{ + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "subnet": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "address_prefix": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "security_group": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + Set: resourceAzureSubnetHash, + }, + + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAzureVirtualNetworkCreate(d *schema.ResourceData, meta interface{}) error { + ac := meta.(*Client) + mc := ac.mgmtClient + + name := d.Get("name").(string) + + // Lock the client just before we get the virtual network configuration and immediately + // set an defer to unlock the client again whenever this function exits + ac.mutex.Lock() + defer ac.mutex.Unlock() + + nc, err := virtualnetwork.NewClient(mc).GetVirtualNetworkConfiguration() + if err != nil { + if strings.Contains(err.Error(), "ResourceNotFound") { + nc = virtualnetwork.NetworkConfiguration{} + } else { + return fmt.Errorf(virtualNetworkRetrievalError, err) + } + } + + for _, n := range nc.Configuration.VirtualNetworkSites { + if n.Name == name { + return fmt.Errorf("Virtual Network %s already exists!", name) + } + } + + network, err := createVirtualNetwork(d) + if err != nil { + return err + } + + nc.Configuration.VirtualNetworkSites = append(nc.Configuration.VirtualNetworkSites, network) + + req, err := virtualnetwork.NewClient(mc).SetVirtualNetworkConfiguration(nc) + if err != nil { + return fmt.Errorf("Error creating Virtual Network %s: %s", name, err) + } + + // Wait until the virtual network is created + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf("Error waiting for Virtual Network %s to be created: %s", name, err) + } + + d.SetId(name) + + if err := associateSecurityGroups(d, meta); err != nil { + return err + } + + return resourceAzureVirtualNetworkRead(d, meta) +} + +func resourceAzureVirtualNetworkRead(d *schema.ResourceData, meta interface{}) error { + mc := meta.(*Client).mgmtClient + + nc, err := virtualnetwork.NewClient(mc).GetVirtualNetworkConfiguration() + if err != nil { + return fmt.Errorf(virtualNetworkRetrievalError, err) + } + + for _, n := range nc.Configuration.VirtualNetworkSites { + if n.Name == d.Id() { + d.Set("address_space", n.AddressSpace.AddressPrefix) + d.Set("location", n.Location) + + // Create a new set to hold all configured subnets + subnets := &schema.Set{ + F: resourceAzureSubnetHash, + } + + // Loop through all endpoints + for _, s := range n.Subnets { + subnet := map[string]interface{}{} + + // Get the associated (if any) security group + sg, err := networksecuritygroup.NewClient(mc). + GetNetworkSecurityGroupForSubnet(s.Name, d.Id()) + if err != nil && !management.IsResourceNotFoundError(err) { + return fmt.Errorf( + "Error retrieving Network Security Group associations of subnet %s: %s", s.Name, err) + } + + // Update the values + subnet["name"] = s.Name + subnet["address_prefix"] = s.AddressPrefix + subnet["security_group"] = sg.Name + + subnets.Add(subnet) + } + + d.Set("subnet", subnets) + + return nil + } + } + + log.Printf("[DEBUG] Virtual Network %s does no longer exist", d.Id()) + d.SetId("") + + return nil +} + +func resourceAzureVirtualNetworkUpdate(d *schema.ResourceData, meta interface{}) error { + ac := meta.(*Client) + mc := ac.mgmtClient + + // Lock the client just before we get the virtual network configuration and immediately + // set an defer to unlock the client again whenever this function exits + ac.mutex.Lock() + defer ac.mutex.Unlock() + + nc, err := virtualnetwork.NewClient(mc).GetVirtualNetworkConfiguration() + if err != nil { + return fmt.Errorf(virtualNetworkRetrievalError, err) + } + + found := false + for i, n := range nc.Configuration.VirtualNetworkSites { + if n.Name == d.Id() { + network, err := createVirtualNetwork(d) + if err != nil { + return err + } + + nc.Configuration.VirtualNetworkSites[i] = network + + found = true + } + } + + if !found { + return fmt.Errorf("Virtual Network %s does not exists!", d.Id()) + } + + req, err := virtualnetwork.NewClient(mc).SetVirtualNetworkConfiguration(nc) + if err != nil { + return fmt.Errorf("Error updating Virtual Network %s: %s", d.Id(), err) + } + + // Wait until the virtual network is updated + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf("Error waiting for Virtual Network %s to be updated: %s", d.Id(), err) + } + + if err := associateSecurityGroups(d, meta); err != nil { + return err + } + + return resourceAzureVirtualNetworkRead(d, meta) +} + +func resourceAzureVirtualNetworkDelete(d *schema.ResourceData, meta interface{}) error { + ac := meta.(*Client) + mc := ac.mgmtClient + + // Lock the client just before we get the virtual network configuration and immediately + // set an defer to unlock the client again whenever this function exits + ac.mutex.Lock() + defer ac.mutex.Unlock() + + nc, err := virtualnetwork.NewClient(mc).GetVirtualNetworkConfiguration() + if err != nil { + return fmt.Errorf(virtualNetworkRetrievalError, err) + } + + filtered := nc.Configuration.VirtualNetworkSites[:0] + for _, n := range nc.Configuration.VirtualNetworkSites { + if n.Name != d.Id() { + filtered = append(filtered, n) + } + } + + nc.Configuration.VirtualNetworkSites = filtered + + req, err := virtualnetwork.NewClient(mc).SetVirtualNetworkConfiguration(nc) + if err != nil { + return fmt.Errorf("Error deleting Virtual Network %s: %s", d.Id(), err) + } + + // Wait until the virtual network is deleted + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf("Error waiting for Virtual Network %s to be deleted: %s", d.Id(), err) + } + + d.SetId("") + + return nil +} + +func resourceAzureSubnetHash(v interface{}) int { + m := v.(map[string]interface{}) + subnet := m["name"].(string) + m["address_prefix"].(string) + m["security_group"].(string) + return hashcode.String(subnet) +} + +func createVirtualNetwork(d *schema.ResourceData) (virtualnetwork.VirtualNetworkSite, error) { + var addressPrefix []string + err := mapstructure.WeakDecode(d.Get("address_space"), &addressPrefix) + if err != nil { + return virtualnetwork.VirtualNetworkSite{}, fmt.Errorf("Error decoding address_space: %s", err) + } + + addressSpace := virtualnetwork.AddressSpace{ + AddressPrefix: addressPrefix, + } + + // Add all subnets that are configured + var subnets []virtualnetwork.Subnet + if rs := d.Get("subnet").(*schema.Set); rs.Len() > 0 { + for _, subnet := range rs.List() { + subnet := subnet.(map[string]interface{}) + subnets = append(subnets, virtualnetwork.Subnet{ + Name: subnet["name"].(string), + AddressPrefix: subnet["address_prefix"].(string), + }) + } + } + + return virtualnetwork.VirtualNetworkSite{ + Name: d.Get("name").(string), + Location: d.Get("location").(string), + AddressSpace: addressSpace, + Subnets: subnets, + }, nil +} + +func associateSecurityGroups(d *schema.ResourceData, meta interface{}) error { + mc := meta.(*Client).mgmtClient + nsgClient := networksecuritygroup.NewClient(mc) + + virtualNetwork := d.Get("name").(string) + + if rs := d.Get("subnet").(*schema.Set); rs.Len() > 0 { + for _, subnet := range rs.List() { + subnet := subnet.(map[string]interface{}) + securityGroup := subnet["security_group"].(string) + subnetName := subnet["name"].(string) + + // Get the associated (if any) security group + sg, err := nsgClient.GetNetworkSecurityGroupForSubnet(subnetName, d.Id()) + if err != nil && !management.IsResourceNotFoundError(err) { + return fmt.Errorf( + "Error retrieving Network Security Group associations of subnet %s: %s", subnetName, err) + } + + // If the desired and actual security group are the same, were done so can just continue + if sg.Name == securityGroup { + continue + } + + // If there is an associated security group, make sure we first remove it from the subnet + if sg.Name != "" { + req, err := nsgClient.RemoveNetworkSecurityGroupFromSubnet(sg.Name, subnetName, virtualNetwork) + if err != nil { + return fmt.Errorf("Error removing Network Security Group %s from subnet %s: %s", + securityGroup, subnetName, err) + } + + // Wait until the security group is associated + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for Network Security Group %s to be removed from subnet %s: %s", + securityGroup, subnetName, err) + } + } + + // If the desired security group is not empty, assign the security group to the subnet + if securityGroup != "" { + req, err := nsgClient.AddNetworkSecurityToSubnet(securityGroup, subnetName, virtualNetwork) + if err != nil { + return fmt.Errorf("Error associating Network Security Group %s to subnet %s: %s", + securityGroup, subnetName, err) + } + + // Wait until the security group is associated + if err := mc.WaitForOperation(req, nil); err != nil { + return fmt.Errorf( + "Error waiting for Network Security Group %s to be associated with subnet %s: %s", + securityGroup, subnetName, err) + } + } + + } + } + + return nil +} diff --git a/builtin/providers/azure/resource_azure_virtual_network_test.go b/builtin/providers/azure/resource_azure_virtual_network_test.go new file mode 100644 index 000000000..69a0362a5 --- /dev/null +++ b/builtin/providers/azure/resource_azure_virtual_network_test.go @@ -0,0 +1,282 @@ +package azure + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/svanharmelen/azure-sdk-for-go/management/virtualnetwork" +) + +func TestAccAzureVirtualNetwork_basic(t *testing.T) { + var network virtualnetwork.VirtualNetworkSite + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureVirtualNetworkDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureVirtualNetwork_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureVirtualNetworkExists( + "azure_virtual_network.foo", &network), + testAccCheckAzureVirtualNetworkAttributes(&network), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "name", "terraform-vnet"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "location", "West US"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "address_space.0", "10.1.2.0/24"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "subnet.1787288781.name", "subnet1"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "subnet.1787288781.address_prefix", "10.1.2.0/25"), + ), + }, + }, + }) +} + +func TestAccAzureVirtualNetwork_advanced(t *testing.T) { + var network virtualnetwork.VirtualNetworkSite + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureVirtualNetworkDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureVirtualNetwork_advanced, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureVirtualNetworkExists( + "azure_virtual_network.foo", &network), + testAccCheckAzureVirtualNetworkAttributes(&network), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "name", "terraform-vnet"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "location", "West US"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "address_space.0", "10.1.2.0/24"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "subnet.33778499.name", "subnet1"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "subnet.33778499.address_prefix", "10.1.2.0/25"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "subnet.33778499.security_group", "terraform-security-group1"), + ), + }, + }, + }) +} + +func TestAccAzureVirtualNetwork_update(t *testing.T) { + var network virtualnetwork.VirtualNetworkSite + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureVirtualNetworkDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureVirtualNetwork_advanced, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureVirtualNetworkExists( + "azure_virtual_network.foo", &network), + testAccCheckAzureVirtualNetworkAttributes(&network), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "name", "terraform-vnet"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "location", "West US"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "address_space.0", "10.1.2.0/24"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "subnet.33778499.name", "subnet1"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "subnet.33778499.address_prefix", "10.1.2.0/25"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "subnet.33778499.security_group", "terraform-security-group1"), + ), + }, + + resource.TestStep{ + Config: testAccAzureVirtualNetwork_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureVirtualNetworkExists( + "azure_virtual_network.foo", &network), + testAccCheckAzureVirtualNetworkAttributes(&network), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "name", "terraform-vnet"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "location", "West US"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "address_space.0", "10.1.3.0/24"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "subnet.514595123.name", "subnet1"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "subnet.514595123.address_prefix", "10.1.3.128/25"), + resource.TestCheckResourceAttr( + "azure_virtual_network.foo", "subnet.514595123.security_group", "terraform-security-group2"), + ), + }, + }, + }) +} + +func testAccCheckAzureVirtualNetworkExists( + n string, + network *virtualnetwork.VirtualNetworkSite) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Virtual Network ID is set") + } + + mc := testAccProvider.Meta().(*Client).mgmtClient + nc, err := virtualnetwork.NewClient(mc).GetVirtualNetworkConfiguration() + if err != nil { + return err + } + + for _, n := range nc.Configuration.VirtualNetworkSites { + if n.Name == rs.Primary.ID { + *network = n + + return nil + } + } + + return fmt.Errorf("Virtual Network not found") + } +} + +func testAccCheckAzureVirtualNetworkAttributes( + network *virtualnetwork.VirtualNetworkSite) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if network.Name != "terraform-vnet" { + return fmt.Errorf("Bad name: %s", network.Name) + } + + if network.Location != "West US" { + return fmt.Errorf("Bad location: %s", network.Location) + } + + return nil + } +} + +func testAccCheckAzureVirtualNetworkDestroy(s *terraform.State) error { + mc := testAccProvider.Meta().(*Client).mgmtClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azure_virtual_network" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Virtual Network ID is set") + } + + nc, err := virtualnetwork.NewClient(mc).GetVirtualNetworkConfiguration() + if err != nil { + return fmt.Errorf("Error retrieving Virtual Network Configuration: %s", err) + } + + for _, n := range nc.Configuration.VirtualNetworkSites { + if n.Name == rs.Primary.ID { + return fmt.Errorf("Resource %s still exists", rs.Primary.ID) + } + } + } + + return nil +} + +const testAccAzureVirtualNetwork_basic = ` +resource "azure_virtual_network" "foo" { + name = "terraform-vnet" + address_space = ["10.1.2.0/24"] + location = "West US" + + subnet { + name = "subnet1" + address_prefix = "10.1.2.0/25" + } +}` + +const testAccAzureVirtualNetwork_advanced = ` +resource "azure_security_group" "foo" { + name = "terraform-security-group1" + location = "West US" + + rule { + name = "RDP" + priority = 101 + source_cidr = "*" + source_port = "*" + destination_cidr = "*" + destination_port = "3389" + protocol = "TCP" + } +} + +resource "azure_virtual_network" "foo" { + name = "terraform-vnet" + address_space = ["10.1.2.0/24"] + location = "West US" + + subnet { + name = "subnet1" + address_prefix = "10.1.2.0/25" + security_group = "${azure_security_group.foo.name}" + } +}` + +const testAccAzureVirtualNetwork_update = ` +resource "azure_security_group" "foo" { + name = "terraform-security-group1" + location = "West US" + + rule { + name = "RDP" + priority = 101 + source_cidr = "*" + source_port = "*" + destination_cidr = "*" + destination_port = "3389" + protocol = "TCP" + } +} + +resource "azure_security_group" "bar" { + name = "terraform-security-group2" + location = "West US" + + rule { + name = "SSH" + priority = 101 + source_cidr = "*" + source_port = "*" + destination_cidr = "*" + destination_port = "22" + protocol = "TCP" + } +} + +resource "azure_virtual_network" "foo" { + name = "terraform-vnet" + address_space = ["10.1.3.0/24"] + location = "West US" + + subnet { + name = "subnet1" + address_prefix = "10.1.3.128/25" + security_group = "${azure_security_group.bar.name}" + } +}` diff --git a/builtin/providers/azure/resources.go b/builtin/providers/azure/resources.go new file mode 100644 index 000000000..6512f735e --- /dev/null +++ b/builtin/providers/azure/resources.go @@ -0,0 +1 @@ +package azure diff --git a/builtin/providers/cloudstack/provider.go b/builtin/providers/cloudstack/provider.go index 8cdafd1ab..73ce38b76 100644 --- a/builtin/providers/cloudstack/provider.go +++ b/builtin/providers/cloudstack/provider.go @@ -45,6 +45,7 @@ func Provider() terraform.ResourceProvider { "cloudstack_network_acl_rule": resourceCloudStackNetworkACLRule(), "cloudstack_nic": resourceCloudStackNIC(), "cloudstack_port_forward": resourceCloudStackPortForward(), + "cloudstack_ssh_keypair": resourceCloudStackSSHKeyPair(), "cloudstack_template": resourceCloudStackTemplate(), "cloudstack_vpc": resourceCloudStackVPC(), "cloudstack_vpn_connection": resourceCloudStackVPNConnection(), diff --git a/builtin/providers/cloudstack/provider_test.go b/builtin/providers/cloudstack/provider_test.go index c9da7dc8a..8d684a673 100644 --- a/builtin/providers/cloudstack/provider_test.go +++ b/builtin/providers/cloudstack/provider_test.go @@ -82,6 +82,8 @@ var CLOUDSTACK_VPC_OFFERING = "" var CLOUDSTACK_VPC_NETWORK_CIDR = "" var CLOUDSTACK_VPC_NETWORK_OFFERING = "" var CLOUDSTACK_PUBLIC_IPADDRESS = "" +var CLOUDSTACK_SSH_KEYPAIR = "" +var CLOUDSTACK_SSH_PUBLIC_KEY = "" var CLOUDSTACK_TEMPLATE = "" var CLOUDSTACK_TEMPLATE_FORMAT = "" var CLOUDSTACK_TEMPLATE_URL = "" diff --git a/builtin/providers/cloudstack/resource_cloudstack_instance.go b/builtin/providers/cloudstack/resource_cloudstack_instance.go index c07678f3e..59079b7de 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_instance.go +++ b/builtin/providers/cloudstack/resource_cloudstack_instance.go @@ -62,6 +62,11 @@ func resourceCloudStackInstance() *schema.Resource { ForceNew: true, }, + "keypair": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "user_data": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -136,6 +141,10 @@ func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{}) p.SetIpaddress(ipaddres.(string)) } + if keypair, ok := d.GetOk("keypair"); ok { + p.SetKeypair(keypair.(string)) + } + // If the user data contains any info, it needs to be base64 encoded and // added to the parameter struct if userData, ok := d.GetOk("user_data"); ok { @@ -186,6 +195,7 @@ func resourceCloudStackInstanceRead(d *schema.ResourceData, meta interface{}) er d.Set("display_name", vm.Displayname) d.Set("ipaddress", vm.Nic[0].Ipaddress) d.Set("zone", vm.Zonename) + //NB cloudstack sometimes sends back the wrong keypair name, so dont update it setValueOrUUID(d, "network", vm.Nic[0].Networkname, vm.Nic[0].Networkid) setValueOrUUID(d, "service_offering", vm.Serviceofferingname, vm.Serviceofferingid) @@ -220,41 +230,58 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) d.SetPartial("display_name") } - // Check if the service offering is changed and if so, update the offering - if d.HasChange("service_offering") { - log.Printf("[DEBUG] Service offering changed for %s, starting update", name) - - // Retrieve the service_offering UUID - serviceofferingid, e := retrieveUUID(cs, "service_offering", d.Get("service_offering").(string)) - if e != nil { - return e.Error() - } - - // Create a new parameter struct - p := cs.VirtualMachine.NewChangeServiceForVirtualMachineParams(d.Id(), serviceofferingid) - - // Before we can actually change the service offering, the virtual machine must be stopped + // Attributes that require reboot to update + if d.HasChange("service_offering") || d.HasChange("keypair") { + // Before we can actually make these changes, the virtual machine must be stopped _, err := cs.VirtualMachine.StopVirtualMachine(cs.VirtualMachine.NewStopVirtualMachineParams(d.Id())) if err != nil { return fmt.Errorf( - "Error stopping instance %s before changing service offering: %s", name, err) + "Error stopping instance %s before making changes: %s", name, err) } - // Change the service offering - _, err = cs.VirtualMachine.ChangeServiceForVirtualMachine(p) - if err != nil { - return fmt.Errorf( - "Error changing the service offering for instance %s: %s", name, err) + + // Check if the service offering is changed and if so, update the offering + if d.HasChange("service_offering") { + log.Printf("[DEBUG] Service offering changed for %s, starting update", name) + + // Retrieve the service_offering UUID + serviceofferingid, e := retrieveUUID(cs, "service_offering", d.Get("service_offering").(string)) + if e != nil { + return e.Error() + } + + // Create a new parameter struct + p := cs.VirtualMachine.NewChangeServiceForVirtualMachineParams(d.Id(), serviceofferingid) + + // Change the service offering + _, err = cs.VirtualMachine.ChangeServiceForVirtualMachine(p) + if err != nil { + return fmt.Errorf( + "Error changing the service offering for instance %s: %s", name, err) + } + d.SetPartial("service_offering") } + + if d.HasChange("keypair") { + log.Printf("[DEBUG] SSH keypair changed for %s, starting update", name) + + p := cs.SSH.NewResetSSHKeyForVirtualMachineParams(d.Id(), d.Get("keypair").(string)) + + // Change the ssh keypair + _, err = cs.SSH.ResetSSHKeyForVirtualMachine(p) + if err != nil { + return fmt.Errorf( + "Error changing the SSH keypair for instance %s: %s", name, err) + } + d.SetPartial("keypair") + } + // Start the virtual machine again _, err = cs.VirtualMachine.StartVirtualMachine(cs.VirtualMachine.NewStartVirtualMachineParams(d.Id())) if err != nil { return fmt.Errorf( - "Error starting instance %s after changing service offering: %s", name, err) + "Error starting instance %s after making changes", name) } - - d.SetPartial("service_offering") } - d.Partial(false) return resourceCloudStackInstanceRead(d, meta) } diff --git a/builtin/providers/cloudstack/resource_cloudstack_instance_test.go b/builtin/providers/cloudstack/resource_cloudstack_instance_test.go index 2d1be9c53..d5ff3fe59 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_instance_test.go +++ b/builtin/providers/cloudstack/resource_cloudstack_instance_test.go @@ -27,6 +27,10 @@ func TestAccCloudStackInstance_basic(t *testing.T) { "cloudstack_instance.foobar", "user_data", "0cf3dcdc356ec8369494cb3991985ecd5296cdd5"), + resource.TestCheckResourceAttr( + "cloudstack_instance.foobar", + "keypair", + CLOUDSTACK_SSH_KEYPAIR), ), }, }, @@ -47,6 +51,14 @@ func TestAccCloudStackInstance_update(t *testing.T) { testAccCheckCloudStackInstanceExists( "cloudstack_instance.foobar", &instance), testAccCheckCloudStackInstanceAttributes(&instance), + resource.TestCheckResourceAttr( + "cloudstack_instance.foobar", + "user_data", + "0cf3dcdc356ec8369494cb3991985ecd5296cdd5"), + resource.TestCheckResourceAttr( + "cloudstack_instance.foobar", + "keypair", + CLOUDSTACK_SSH_KEYPAIR), ), }, @@ -193,13 +205,15 @@ resource "cloudstack_instance" "foobar" { network = "%s" template = "%s" zone = "%s" + keypair = "%s" user_data = "foobar\nfoo\nbar" expunge = true }`, CLOUDSTACK_SERVICE_OFFERING_1, CLOUDSTACK_NETWORK_1, CLOUDSTACK_TEMPLATE, - CLOUDSTACK_ZONE) + CLOUDSTACK_ZONE, + CLOUDSTACK_SSH_KEYPAIR) var testAccCloudStackInstance_renameAndResize = fmt.Sprintf(` resource "cloudstack_instance" "foobar" { @@ -209,13 +223,15 @@ resource "cloudstack_instance" "foobar" { network = "%s" template = "%s" zone = "%s" + keypair = "%s" user_data = "foobar\nfoo\nbar" expunge = true }`, CLOUDSTACK_SERVICE_OFFERING_2, CLOUDSTACK_NETWORK_1, CLOUDSTACK_TEMPLATE, - CLOUDSTACK_ZONE) + CLOUDSTACK_ZONE, + CLOUDSTACK_SSH_KEYPAIR) var testAccCloudStackInstance_fixedIP = fmt.Sprintf(` resource "cloudstack_instance" "foobar" { diff --git a/builtin/providers/cloudstack/resource_cloudstack_ssh_keypair.go b/builtin/providers/cloudstack/resource_cloudstack_ssh_keypair.go new file mode 100644 index 000000000..12a7f1b38 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_ssh_keypair.go @@ -0,0 +1,122 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackSSHKeyPair() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackSSHKeyPairCreate, + Read: resourceCloudStackSSHKeyPairRead, + Delete: resourceCloudStackSSHKeyPairDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "public_key": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "private_key": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "fingerprint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceCloudStackSSHKeyPairCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + publicKey := d.Get("public_key").(string) + + if publicKey != "" { + //register key supplied + p := cs.SSH.NewRegisterSSHKeyPairParams(name, publicKey) + r, err := cs.SSH.RegisterSSHKeyPair(p) + if err != nil { + return err + } + log.Printf("[DEBUG] RegisterSSHKeyPair response: %+v\n", r) + log.Printf("[DEBUG] Key pair successfully registered at Cloudstack") + d.SetId(name) + } else { + //no key supplied, must create one and return the private key + p := cs.SSH.NewCreateSSHKeyPairParams(name) + r, err := cs.SSH.CreateSSHKeyPair(p) + if err != nil { + return err + } + log.Printf("[DEBUG] CreateSSHKeyPair response: %+v\n", r) + log.Printf("[DEBUG] Key pair successfully generated at Cloudstack") + log.Printf("[DEBUG] Private key returned: %s", r.Privatekey) + d.Set("private_key", r.Privatekey) + d.SetId(name) + } + + return resourceCloudStackSSHKeyPairRead(d, meta) +} + +func resourceCloudStackSSHKeyPairRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + log.Printf("[DEBUG] looking for ssh key %s with name %s", d.Id(), d.Get("name").(string)) + + p := cs.SSH.NewListSSHKeyPairsParams() + p.SetName(d.Get("name").(string)) + + r, err := cs.SSH.ListSSHKeyPairs(p) + if err != nil { + return err + } + if r.Count == 0 { + log.Printf("[DEBUG] Key pair %s does not exist", d.Get("name").(string)) + d.Set("name", "") + return nil + } + + //SSHKeyPair name is unique in a cloudstack account so dont need to check for multiple + d.Set("name", r.SSHKeyPairs[0].Name) + d.Set("fingerprint", r.SSHKeyPairs[0].Fingerprint) + log.Printf("[DEBUG] Read ssh key pair %+v\n", d) + + return nil +} + +func resourceCloudStackSSHKeyPairDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.SSH.NewDeleteSSHKeyPairParams(d.Get("name").(string)) + + // Remove the SSH Keypair + _, err := cs.SSH.DeleteSSHKeyPair(p) + if err != nil { + // This is a very poor way to be told the UUID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "A key pair with name '%s' does not exist for account", d.Get("name").(string))) { + return nil + } + + return fmt.Errorf("Error deleting SSH Keypair: %s", err) + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_ssh_keypair_test.go b/builtin/providers/cloudstack/resource_cloudstack_ssh_keypair_test.go new file mode 100644 index 000000000..45fbc2c06 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_ssh_keypair_test.go @@ -0,0 +1,160 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackSSHKeyPair_basic(t *testing.T) { + var sshkey cloudstack.SSHKeyPair + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackSSHKeyPairDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackSSHKeyPair_create, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackSSHKeyPairExists("cloudstack_ssh_keypair.foo", &sshkey), + testAccCheckCloudStackSSHKeyPairAttributes(&sshkey), + testAccCheckCloudStackSSHKeyPairCreateAttributes("cloudstack_ssh_keypair.foo"), + ), + }, + }, + }) +} + +func TestAccCloudStackSSHKeyPair_register(t *testing.T) { + var sshkey cloudstack.SSHKeyPair + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackSSHKeyPairDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackSSHKeyPair_register, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackSSHKeyPairExists("cloudstack_ssh_keypair.foo", &sshkey), + testAccCheckCloudStackSSHKeyPairAttributes(&sshkey), + resource.TestCheckResourceAttr( + "cloudstack_ssh_keypair.foo", + "public_key", + CLOUDSTACK_SSH_PUBLIC_KEY), + ), + }, + }, + }) +} + +func testAccCheckCloudStackSSHKeyPairExists(n string, sshkey *cloudstack.SSHKeyPair) 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.Attributes["name"] == "" { + return fmt.Errorf("No ssh key name is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + p := cs.SSH.NewListSSHKeyPairsParams() + p.SetName(rs.Primary.Attributes["name"]) + list, err := cs.SSH.ListSSHKeyPairs(p) + + if err != nil { + return err + } + + if list.Count == 1 && list.SSHKeyPairs[0].Name == rs.Primary.Attributes["name"] { + //ssh key exists + *sshkey = *list.SSHKeyPairs[0] + return nil + } + + return fmt.Errorf("SSH key not found") + } +} + +func testAccCheckCloudStackSSHKeyPairAttributes( + sshkey *cloudstack.SSHKeyPair) resource.TestCheckFunc { + return func(s *terraform.State) error { + + fingerprintLen := len(sshkey.Fingerprint) + if fingerprintLen != 47 { + return fmt.Errorf( + "SSH key: Attribute private_key expected length 47, got %d", + fingerprintLen) + } + + return nil + } +} + +func testAccCheckCloudStackSSHKeyPairCreateAttributes( + name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + ms := s.RootModule() + rs, ok := ms.Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + is := rs.Primary + if is == nil { + return fmt.Errorf("No primary instance: %s", name) + } + + log.Printf("Private key calculated: %s", is.Attributes["private_key"]) + if !strings.Contains(is.Attributes["private_key"], "PRIVATE KEY") { + return fmt.Errorf( + "SSH key: Attribute private_key expected 'PRIVATE KEY' to be present, got %s", + is.Attributes["private_key"]) + } + return nil + } +} + +func testAccCheckCloudStackSSHKeyPairDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_ssh_keypair" { + continue + } + + if rs.Primary.Attributes["name"] == "" { + return fmt.Errorf("No ssh key name is set") + } + + p := cs.SSH.NewDeleteSSHKeyPairParams(rs.Primary.Attributes["name"]) + _, err := cs.SSH.DeleteSSHKeyPair(p) + + if err != nil { + return fmt.Errorf( + "Error deleting ssh key (%s): %s", + rs.Primary.Attributes["name"], err) + } + } + + return nil +} + +var testAccCloudStackSSHKeyPair_create = fmt.Sprintf(` +resource "cloudstack_ssh_keypair" "foo" { + name = "terraform-testacc" +}`) + +var testAccCloudStackSSHKeyPair_register = fmt.Sprintf(` +resource "cloudstack_ssh_keypair" "foo" { + name = "terraform-testacc" + public_key = "%s" +}`, CLOUDSTACK_SSH_PUBLIC_KEY) diff --git a/builtin/providers/cloudstack/resource_cloudstack_template.go b/builtin/providers/cloudstack/resource_cloudstack_template.go index f50316027..15c6ebec4 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_template.go +++ b/builtin/providers/cloudstack/resource_cloudstack_template.go @@ -292,9 +292,9 @@ func resourceCloudStackTemplateDelete(d *schema.ResourceData, meta interface{}) func verifyTemplateParams(d *schema.ResourceData) error { format := d.Get("format").(string) - if format != "QCOW2" && format != "RAW" && format != "VHD" && format != "VMDK" { + if format != "OVA" && format != "QCOW2" && format != "RAW" && format != "VHD" && format != "VMDK" { return fmt.Errorf( - "%s is not a valid format. Valid options are 'QCOW2', 'RAW', 'VHD' and 'VMDK'", format) + "%s is not a valid format. Valid options are 'OVA','QCOW2', 'RAW', 'VHD' and 'VMDK'", format) } return nil diff --git a/builtin/providers/consul/resource_consul_keys_test.go b/builtin/providers/consul/resource_consul_keys_test.go index e1a959b7d..97890335c 100644 --- a/builtin/providers/consul/resource_consul_keys_test.go +++ b/builtin/providers/consul/resource_consul_keys_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform/terraform" ) -func TestAccConsulKeys(t *testing.T) { +func TestAccConsulKeys_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() {}, Providers: testAccProviders, diff --git a/builtin/providers/dme/resource_dme_record_test.go b/builtin/providers/dme/resource_dme_record_test.go index 284ab0ad9..430afacb2 100644 --- a/builtin/providers/dme/resource_dme_record_test.go +++ b/builtin/providers/dme/resource_dme_record_test.go @@ -13,7 +13,7 @@ import ( var _ = fmt.Sprintf("dummy") // dummy var _ = os.DevNull // dummy -func TestAccDMERecordA(t *testing.T) { +func TestAccDMERecord_basic(t *testing.T) { var record dnsmadeeasy.Record domainid := os.Getenv("DME_DOMAINID") diff --git a/builtin/providers/docker/resource_docker_container.go b/builtin/providers/docker/resource_docker_container.go index 10481b268..59e65b9c1 100644 --- a/builtin/providers/docker/resource_docker_container.go +++ b/builtin/providers/docker/resource_docker_container.go @@ -136,6 +136,12 @@ func resourceDockerContainer() *schema.Resource { Type: schema.TypeString, Computed: true, }, + + "privileged": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, }, } } diff --git a/builtin/providers/docker/resource_docker_container_funcs.go b/builtin/providers/docker/resource_docker_container_funcs.go index c6bd9dea8..4f642d6dd 100644 --- a/builtin/providers/docker/resource_docker_container_funcs.go +++ b/builtin/providers/docker/resource_docker_container_funcs.go @@ -85,6 +85,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err d.SetId(retContainer.ID) hostConfig := &dc.HostConfig{ + Privileged: d.Get("privileged").(bool), PublishAllPorts: d.Get("publish_all_ports").(bool), } diff --git a/builtin/providers/google/resource_storage_bucket_test.go b/builtin/providers/google/resource_storage_bucket_test.go index e33cd5cb7..a7b59c61a 100644 --- a/builtin/providers/google/resource_storage_bucket_test.go +++ b/builtin/providers/google/resource_storage_bucket_test.go @@ -1,9 +1,9 @@ package google import ( + "bytes" "fmt" "math/rand" - "bytes" "testing" "time" @@ -14,7 +14,7 @@ import ( storage "google.golang.org/api/storage/v1" ) -func TestAccStorageDefaults(t *testing.T) { +func TestAccStorage_basic(t *testing.T) { var bucketName string resource.Test(t, resource.TestCase{ diff --git a/builtin/providers/openstack/config.go b/builtin/providers/openstack/config.go index 903345fb6..f18465538 100644 --- a/builtin/providers/openstack/config.go +++ b/builtin/providers/openstack/config.go @@ -2,6 +2,7 @@ package openstack import ( "crypto/tls" + "fmt" "net/http" "github.com/rackspace/gophercloud" @@ -19,11 +20,20 @@ type Config struct { DomainID string DomainName string Insecure bool + EndpointType string osClient *gophercloud.ProviderClient } func (c *Config) loadAndValidate() error { + + if c.EndpointType != "internal" && c.EndpointType != "internalURL" && + c.EndpointType != "admin" && c.EndpointType != "adminURL" && + c.EndpointType != "public" && c.EndpointType != "publicURL" && + c.EndpointType != "" { + return fmt.Errorf("Invalid endpoint type provided") + } + ao := gophercloud.AuthOptions{ Username: c.Username, UserID: c.UserID, @@ -60,24 +70,38 @@ func (c *Config) loadAndValidate() error { func (c *Config) blockStorageV1Client(region string) (*gophercloud.ServiceClient, error) { return openstack.NewBlockStorageV1(c.osClient, gophercloud.EndpointOpts{ - Region: region, + Region: region, + Availability: c.getEndpointType(), }) } func (c *Config) computeV2Client(region string) (*gophercloud.ServiceClient, error) { return openstack.NewComputeV2(c.osClient, gophercloud.EndpointOpts{ - Region: region, + Region: region, + Availability: c.getEndpointType(), }) } func (c *Config) networkingV2Client(region string) (*gophercloud.ServiceClient, error) { return openstack.NewNetworkV2(c.osClient, gophercloud.EndpointOpts{ - Region: region, + Region: region, + Availability: c.getEndpointType(), }) } func (c *Config) objectStorageV1Client(region string) (*gophercloud.ServiceClient, error) { return openstack.NewObjectStorageV1(c.osClient, gophercloud.EndpointOpts{ - Region: region, + Region: region, + Availability: c.getEndpointType(), }) } + +func (c *Config) getEndpointType() gophercloud.Availability { + if c.EndpointType == "internal" || c.EndpointType == "internalURL" { + return gophercloud.AvailabilityInternal + } + if c.EndpointType == "admin" || c.EndpointType == "adminURL" { + return gophercloud.AvailabilityAdmin + } + return gophercloud.AvailabilityPublic +} diff --git a/builtin/providers/openstack/provider.go b/builtin/providers/openstack/provider.go index 188beadd7..4a21d69e2 100644 --- a/builtin/providers/openstack/provider.go +++ b/builtin/providers/openstack/provider.go @@ -42,9 +42,9 @@ func Provider() terraform.ResourceProvider { DefaultFunc: envDefaultFunc("OS_PASSWORD"), }, "api_key": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "", + Type: schema.TypeString, + Optional: true, + DefaultFunc: envDefaultFunc("OS_AUTH_TOKEN"), }, "domain_id": &schema.Schema{ Type: schema.TypeString, @@ -61,6 +61,11 @@ func Provider() terraform.ResourceProvider { Optional: true, Default: false, }, + "endpoint_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: envDefaultFunc("OS_ENDPOINT_TYPE"), + }, }, ResourcesMap: map[string]*schema.Resource{ @@ -99,6 +104,7 @@ func configureProvider(d *schema.ResourceData) (interface{}, error) { DomainID: d.Get("domain_id").(string), DomainName: d.Get("domain_name").(string), Insecure: d.Get("insecure").(bool), + EndpointType: d.Get("endpoint_type").(string), } if err := config.loadAndValidate(); err != nil { diff --git a/builtin/providers/openstack/resource_openstack_fw_firewall_v1_test.go b/builtin/providers/openstack/resource_openstack_fw_firewall_v1_test.go index 34112f778..43318db19 100644 --- a/builtin/providers/openstack/resource_openstack_fw_firewall_v1_test.go +++ b/builtin/providers/openstack/resource_openstack_fw_firewall_v1_test.go @@ -11,7 +11,7 @@ import ( "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls" ) -func TestAccFWFirewallV1(t *testing.T) { +func TestAccFWFirewallV1_basic(t *testing.T) { var policyID *string diff --git a/builtin/providers/openstack/resource_openstack_fw_rule_v1_test.go b/builtin/providers/openstack/resource_openstack_fw_rule_v1_test.go index ba96bb8b1..677e7cd01 100644 --- a/builtin/providers/openstack/resource_openstack_fw_rule_v1_test.go +++ b/builtin/providers/openstack/resource_openstack_fw_rule_v1_test.go @@ -12,7 +12,7 @@ import ( "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules" ) -func TestAccFWRuleV1(t *testing.T) { +func TestAccFWRuleV1_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, diff --git a/builtin/provisioners/chef/resource_provisioner.go b/builtin/provisioners/chef/resource_provisioner.go index 2a8e0def6..cb5f5d6ea 100644 --- a/builtin/provisioners/chef/resource_provisioner.go +++ b/builtin/provisioners/chef/resource_provisioner.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform/communicator" "github.com/hashicorp/terraform/communicator/remote" "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/go-homedir" "github.com/mitchellh/go-linereader" "github.com/mitchellh/mapstructure" ) @@ -182,6 +183,15 @@ func (r *ResourceProvisioner) decodeConfig(c *terraform.ResourceConfig) (*Provis return nil, err } + // We need to decode this twice. Once for the Raw config and once + // for the parsed Config. This makes sure that all values are there + // even if some still need to be interpolated later on. + // Without this the validation will fail when using a variable for + // a required parameter (the node_name for example). + if err := dec.Decode(c.Raw); err != nil { + return nil, err + } + if err := dec.Decode(c.Config); err != nil { return nil, err } @@ -190,6 +200,14 @@ func (r *ResourceProvisioner) decodeConfig(c *terraform.ResourceConfig) (*Provis p.Environment = defaultEnv } + if p.ValidationKeyPath != "" { + keyPath, err := homedir.Expand(p.ValidationKeyPath) + if err != nil { + return nil, fmt.Errorf("Error expanding the validation key path: %v", err) + } + p.ValidationKeyPath = keyPath + } + if attrs, ok := c.Config["attributes"]; ok { p.Attributes, err = rawToJSON(attrs) if err != nil { diff --git a/command/apply.go b/command/apply.go index 529d6e701..7664fc0e8 100644 --- a/command/apply.go +++ b/command/apply.go @@ -7,6 +7,7 @@ import ( "sort" "strings" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/terraform" ) @@ -207,7 +208,7 @@ func (c *ApplyCommand) Run(args []string) int { "Instead, your Terraform state file has been partially updated with\n"+ "any resources that successfully completed. Please address the error\n"+ "above and apply again to incrementally change your infrastructure.", - applyErr)) + multierror.Flatten(applyErr))) return 1 } diff --git a/command/output.go b/command/output.go index 2e3c1bee0..1f48d8877 100644 --- a/command/output.go +++ b/command/output.go @@ -15,9 +15,12 @@ type OutputCommand struct { func (c *OutputCommand) Run(args []string) int { args = c.Meta.process(args, false) + var module string cmdFlags := flag.NewFlagSet("output", flag.ContinueOnError) cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") + cmdFlags.StringVar(&module, "module", "", "module") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } + if err := cmdFlags.Parse(args); err != nil { return 1 } @@ -38,15 +41,34 @@ func (c *OutputCommand) Run(args []string) int { return 1 } + if module == "" { + module = "root" + } else { + module = "root." + module + } + + // Get the proper module we want to get outputs for + modPath := strings.Split(module, ".") + state := stateStore.State() - if state.Empty() || len(state.RootModule().Outputs) == 0 { + mod := state.ModuleByPath(modPath) + + if mod == nil { + c.Ui.Error(fmt.Sprintf( + "The module %s could not be found. There is nothing to output.", + module)) + return 1 + } + + if state.Empty() || len(mod.Outputs) == 0 { c.Ui.Error(fmt.Sprintf( "The state file has no outputs defined. Define an output\n" + "in your configuration with the `output` directive and re-run\n" + "`terraform apply` for it to become available.")) return 1 } - v, ok := state.RootModule().Outputs[name] + + v, ok := mod.Outputs[name] if !ok { c.Ui.Error(fmt.Sprintf( "The output variable requested could not be found in the state\n" + diff --git a/command/output_test.go b/command/output_test.go index d3444c389..f84642269 100644 --- a/command/output_test.go +++ b/command/output_test.go @@ -47,6 +47,83 @@ func TestOutput(t *testing.T) { } } +func TestModuleOutput(t *testing.T) { + originalState := &terraform.State{ + Modules: []*terraform.ModuleState{ + &terraform.ModuleState{ + Path: []string{"root"}, + Outputs: map[string]string{ + "foo": "bar", + }, + }, + &terraform.ModuleState{ + Path: []string{"root", "my_module"}, + Outputs: map[string]string{ + "blah": "tastatur", + }, + }, + }, + } + + statePath := testStateFile(t, originalState) + + ui := new(cli.MockUi) + c := &OutputCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "-module", "my_module", + "blah", + } + + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + actual := strings.TrimSpace(ui.OutputWriter.String()) + if actual != "tastatur" { + t.Fatalf("bad: %#v", actual) + } +} + +func TestMissingModuleOutput(t *testing.T) { + originalState := &terraform.State{ + Modules: []*terraform.ModuleState{ + &terraform.ModuleState{ + Path: []string{"root"}, + Outputs: map[string]string{ + "foo": "bar", + }, + }, + }, + } + + statePath := testStateFile(t, originalState) + + ui := new(cli.MockUi) + c := &OutputCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "-module", "not_existing_module", + "blah", + } + + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + func TestOutput_badVar(t *testing.T) { originalState := &terraform.State{ Modules: []*terraform.ModuleState{ diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go index 7c4d76619..2249a9928 100644 --- a/config/interpolate_funcs.go +++ b/config/interpolate_funcs.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "regexp" + "sort" "strconv" "strings" @@ -278,3 +279,73 @@ func interpolationFuncElement() ast.Function { }, } } + +// interpolationFuncKeys implements the "keys" function that yields a list of +// keys of map types within a Terraform configuration. +func interpolationFuncKeys(vs map[string]ast.Variable) ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ast.TypeString}, + ReturnType: ast.TypeString, + Callback: func(args []interface{}) (interface{}, error) { + // Prefix must include ending dot to be a map + prefix := fmt.Sprintf("var.%s.", args[0].(string)) + keys := make([]string, 0, len(vs)) + for k, _ := range vs { + if !strings.HasPrefix(k, prefix) { + continue + } + keys = append(keys, k[len(prefix):]) + } + + if len(keys) <= 0 { + return "", fmt.Errorf( + "failed to find map '%s'", + args[0].(string)) + } + + sort.Strings(keys) + + return strings.Join(keys, InterpSplitDelim), nil + }, + } +} + +// interpolationFuncValues implements the "values" function that yields a list of +// keys of map types within a Terraform configuration. +func interpolationFuncValues(vs map[string]ast.Variable) ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ast.TypeString}, + ReturnType: ast.TypeString, + Callback: func(args []interface{}) (interface{}, error) { + // Prefix must include ending dot to be a map + prefix := fmt.Sprintf("var.%s.", args[0].(string)) + keys := make([]string, 0, len(vs)) + for k, _ := range vs { + if !strings.HasPrefix(k, prefix) { + continue + } + keys = append(keys, k) + } + + if len(keys) <= 0 { + return "", fmt.Errorf( + "failed to find map '%s'", + args[0].(string)) + } + + sort.Strings(keys) + + vals := make([]string, 0, len(keys)) + + for _, k := range keys { + v := vs[k] + if v.Type != ast.TypeString { + return "", fmt.Errorf("values(): %q has bad type %s", k, v.Type) + } + vals = append(vals, vs[k].Value.(string)) + } + + return strings.Join(vals, InterpSplitDelim), nil + }, + } +} diff --git a/config/interpolate_funcs_test.go b/config/interpolate_funcs_test.go index 5432cdbd7..9b8726a3e 100644 --- a/config/interpolate_funcs_test.go +++ b/config/interpolate_funcs_test.go @@ -384,6 +384,104 @@ func TestInterpolateFuncLookup(t *testing.T) { }) } +func TestInterpolateFuncKeys(t *testing.T) { + testFunction(t, testFunctionConfig{ + Vars: map[string]ast.Variable{ + "var.foo.bar": ast.Variable{ + Value: "baz", + Type: ast.TypeString, + }, + "var.foo.qux": ast.Variable{ + Value: "quack", + Type: ast.TypeString, + }, + "var.str": ast.Variable{ + Value: "astring", + Type: ast.TypeString, + }, + }, + Cases: []testFunctionCase{ + { + `${keys("foo")}`, + fmt.Sprintf( + "bar%squx", + InterpSplitDelim), + false, + }, + + // Invalid key + { + `${keys("not")}`, + nil, + true, + }, + + // Too many args + { + `${keys("foo", "bar")}`, + nil, + true, + }, + + // Not a map + { + `${keys("str")}`, + nil, + true, + }, + }, + }) +} + +func TestInterpolateFuncValues(t *testing.T) { + testFunction(t, testFunctionConfig{ + Vars: map[string]ast.Variable{ + "var.foo.bar": ast.Variable{ + Value: "quack", + Type: ast.TypeString, + }, + "var.foo.qux": ast.Variable{ + Value: "baz", + Type: ast.TypeString, + }, + "var.str": ast.Variable{ + Value: "astring", + Type: ast.TypeString, + }, + }, + Cases: []testFunctionCase{ + { + `${values("foo")}`, + fmt.Sprintf( + "quack%sbaz", + InterpSplitDelim), + false, + }, + + // Invalid key + { + `${values("not")}`, + nil, + true, + }, + + // Too many args + { + `${values("foo", "bar")}`, + nil, + true, + }, + + // Not a map + { + `${values("str")}`, + nil, + true, + }, + }, + }) +} + func TestInterpolateFuncElement(t *testing.T) { testFunction(t, testFunctionConfig{ Cases: []testFunctionCase{ diff --git a/config/raw_config.go b/config/raw_config.go index d51578e0b..ebb9f18dc 100644 --- a/config/raw_config.go +++ b/config/raw_config.go @@ -304,6 +304,8 @@ func langEvalConfig(vs map[string]ast.Variable) *lang.EvalConfig { funcMap[k] = v } funcMap["lookup"] = interpolationFuncLookup(vs) + funcMap["keys"] = interpolationFuncKeys(vs) + funcMap["values"] = interpolationFuncValues(vs) return &lang.EvalConfig{ GlobalScope: &ast.BasicScope{ diff --git a/examples/aws-count/README.md b/examples/aws-count/README.md index a2703f59d..102841b94 100644 --- a/examples/aws-count/README.md +++ b/examples/aws-count/README.md @@ -6,5 +6,8 @@ and let you scale resources by simply incrementing a number. Additionally, variables can be used to expand a list of resources for use elsewhere. -As with all examples, just copy and paste the example and run -`terraform apply` to see it work. +To run, configure your AWS provider as described in https://www.terraform.io/docs/providers/aws/index.html + +Running the example + +run `terraform apply` to see it work. diff --git a/examples/aws-two-tier/README.md b/examples/aws-two-tier/README.md index 8a5ad61a3..dcdf7d1c4 100644 --- a/examples/aws-two-tier/README.md +++ b/examples/aws-two-tier/README.md @@ -14,5 +14,19 @@ After you run `terraform apply` on this configuration, it will automatically output the DNS address of the ELB. After your instance registers, this should respond with the default nginx web page. -As with all examples, just copy and paste the example and run -`terraform apply` to see it work. +To run, configure your AWS provider as described in + +https://www.terraform.io/docs/providers/aws/index.html + +Run with a command like this: + +``` +terraform apply -var 'key_name={your_aws_key_name}' \ + -var 'key_path={location_of_your_key_in_your_local_machine}'` +``` + +For example: + +``` +terraform apply -var 'key_name=terraform' -var 'key_path=/Users/jsmith/.ssh/terraform.pem' +``` diff --git a/helper/schema/set.go b/helper/schema/set.go index 7176b8630..8d21866df 100644 --- a/helper/schema/set.go +++ b/helper/schema/set.go @@ -74,7 +74,7 @@ func (s *Set) List() []interface{} { return result } -// Differences performs a set difference of the two sets, returning +// Difference performs a set difference of the two sets, returning // a new third set that has only the elements unique to this set. func (s *Set) Difference(other *Set) *Set { result := &Set{F: s.F} diff --git a/helper/schema/valuetype_string.go b/helper/schema/valuetype_string.go index fec00944e..42442a46b 100644 --- a/helper/schema/valuetype_string.go +++ b/helper/schema/valuetype_string.go @@ -9,7 +9,7 @@ const _ValueType_name = "TypeInvalidTypeBoolTypeIntTypeFloatTypeStringTypeListTy var _ValueType_index = [...]uint8{0, 11, 19, 26, 35, 45, 53, 60, 67, 77} func (i ValueType) String() string { - if i < 0 || i+1 >= ValueType(len(_ValueType_index)) { + if i < 0 || i >= ValueType(len(_ValueType_index)-1) { return fmt.Sprintf("ValueType(%d)", i) } return _ValueType_name[_ValueType_index[i]:_ValueType_index[i+1]] diff --git a/state/remote/http.go b/state/remote/http.go index 8b410fb3d..1aca7652f 100644 --- a/state/remote/http.go +++ b/state/remote/http.go @@ -3,11 +3,13 @@ package remote import ( "bytes" "crypto/md5" + "crypto/tls" "encoding/base64" "fmt" "io" "net/http" "net/url" + "strconv" ) func httpFactory(conf map[string]string) (Client, error) { @@ -24,18 +26,38 @@ func httpFactory(conf map[string]string) (Client, error) { return nil, fmt.Errorf("address must be HTTP or HTTPS") } + client := &http.Client{} + if skipRaw, ok := conf["skip_cert_verification"]; ok { + skip, err := strconv.ParseBool(skipRaw) + if err != nil { + return nil, fmt.Errorf("skip_cert_verification must be boolean") + } + if skip { + // Replace the client with one that ignores TLS verification + client = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + } + } + return &HTTPClient{ - URL: url, + URL: url, + Client: client, }, nil } -// HTTPClient is a remote client that stores data in Consul. +// HTTPClient is a remote client that stores data in Consul or HTTP REST. type HTTPClient struct { - URL *url.URL + URL *url.URL + Client *http.Client } func (c *HTTPClient) Get() (*Payload, error) { - resp, err := http.Get(c.URL.String()) + resp, err := c.Client.Get(c.URL.String()) if err != nil { return nil, err } @@ -110,7 +132,6 @@ func (c *HTTPClient) Put(data []byte) error { } */ - // Make the HTTP client and request req, err := http.NewRequest("POST", base.String(), bytes.NewReader(data)) if err != nil { return fmt.Errorf("Failed to make HTTP request: %s", err) @@ -122,7 +143,7 @@ func (c *HTTPClient) Put(data []byte) error { req.ContentLength = int64(len(data)) // Make the request - resp, err := http.DefaultClient.Do(req) + resp, err := c.Client.Do(req) if err != nil { return fmt.Errorf("Failed to upload state: %v", err) } @@ -138,14 +159,13 @@ func (c *HTTPClient) Put(data []byte) error { } func (c *HTTPClient) Delete() error { - // Make the HTTP request req, err := http.NewRequest("DELETE", c.URL.String(), nil) if err != nil { return fmt.Errorf("Failed to make HTTP request: %s", err) } // Make the request - resp, err := http.DefaultClient.Do(req) + resp, err := c.Client.Do(req) if err != nil { return fmt.Errorf("Failed to delete state: %s", err) } diff --git a/state/remote/http_test.go b/state/remote/http_test.go index e5cdbed88..e6e7297c1 100644 --- a/state/remote/http_test.go +++ b/state/remote/http_test.go @@ -24,7 +24,7 @@ func TestHTTPClient(t *testing.T) { t.Fatalf("err: %s", err) } - client := &HTTPClient{URL: url} + client := &HTTPClient{URL: url, Client: http.DefaultClient} testClient(t, client) } diff --git a/state/remote/remote.go b/state/remote/remote.go index cb525e6a3..7ebea3222 100644 --- a/state/remote/remote.go +++ b/state/remote/remote.go @@ -40,6 +40,7 @@ var BuiltinClients = map[string]Factory{ "consul": consulFactory, "http": httpFactory, "s3": s3Factory, + "swift": swiftFactory, // This is used for development purposes only. "_local": fileFactory, diff --git a/state/remote/s3.go b/state/remote/s3.go index 742bfac76..67f96f39b 100644 --- a/state/remote/s3.go +++ b/state/remote/s3.go @@ -6,10 +6,10 @@ import ( "io" "os" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/aws/credentials" - "github.com/awslabs/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/service/s3" ) func s3Factory(conf map[string]string) (Client, error) { diff --git a/state/remote/s3_test.go b/state/remote/s3_test.go index ca7ec4879..172ed3a0a 100644 --- a/state/remote/s3_test.go +++ b/state/remote/s3_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/awslabs/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3" ) func TestS3Client_impl(t *testing.T) { diff --git a/state/remote/swift.go b/state/remote/swift.go new file mode 100644 index 000000000..a4293f737 --- /dev/null +++ b/state/remote/swift.go @@ -0,0 +1,114 @@ +package remote + +import ( + "bytes" + "crypto/md5" + "fmt" + "os" + "strings" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" +) + +const TFSTATE_NAME = "tfstate.tf" + +// SwiftClient implements the Client interface for an Openstack Swift server. +type SwiftClient struct { + client *gophercloud.ServiceClient + path string +} + +func swiftFactory(conf map[string]string) (Client, error) { + client := &SwiftClient{} + + if err := client.validateConfig(conf); err != nil { + return nil, err + } + + return client, nil +} + +func (c *SwiftClient) validateConfig(conf map[string]string) (err error) { + if val := os.Getenv("OS_AUTH_URL"); val == "" { + return fmt.Errorf("missing OS_AUTH_URL environment variable") + } + if val := os.Getenv("OS_USERNAME"); val == "" { + return fmt.Errorf("missing OS_USERNAME environment variable") + } + if val := os.Getenv("OS_TENANT_NAME"); val == "" { + return fmt.Errorf("missing OS_TENANT_NAME environment variable") + } + if val := os.Getenv("OS_PASSWORD"); val == "" { + return fmt.Errorf("missing OS_PASSWORD environment variable") + } + path, ok := conf["path"] + if !ok || path == "" { + return fmt.Errorf("missing 'path' configuration") + } + + provider, err := openstack.AuthenticatedClient(gophercloud.AuthOptions{ + IdentityEndpoint: os.Getenv("OS_AUTH_URL"), + Username: os.Getenv("OS_USERNAME"), + TenantName: os.Getenv("OS_TENANT_NAME"), + Password: os.Getenv("OS_PASSWORD"), + }) + + if err != nil { + return err + } + + c.path = path + c.client, err = openstack.NewObjectStorageV1(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + + return err +} + +func (c *SwiftClient) Get() (*Payload, error) { + result := objects.Download(c.client, c.path, TFSTATE_NAME, nil) + bytes, err := result.ExtractContent() + + if err != nil { + if strings.Contains(err.Error(), "but got 404 instead") { + return nil, nil + } + return nil, err + } + + hash := md5.Sum(bytes) + payload := &Payload{ + Data: bytes, + MD5: hash[:md5.Size], + } + + return payload, nil +} + +func (c *SwiftClient) Put(data []byte) error { + if err := c.ensureContainerExists(); err != nil { + return err + } + + reader := bytes.NewReader(data) + result := objects.Create(c.client, c.path, TFSTATE_NAME, reader, nil) + + return result.Err +} + +func (c *SwiftClient) Delete() error { + result := objects.Delete(c.client, c.path, TFSTATE_NAME, nil) + return result.Err +} + +func (c *SwiftClient) ensureContainerExists() error { + result := containers.Create(c.client, c.path, nil) + if result.Err != nil { + return result.Err + } + + return nil +} diff --git a/state/remote/swift_test.go b/state/remote/swift_test.go new file mode 100644 index 000000000..6ed4fd596 --- /dev/null +++ b/state/remote/swift_test.go @@ -0,0 +1,31 @@ +package remote + +import ( + "net/http" + "testing" + "os" +) + +func TestSwiftClient_impl(t *testing.T) { + var _ Client = new(SwiftClient) +} + +func TestSwiftClient(t *testing.T) { + os_auth_url := os.Getenv("OS_AUTH_URL") + if os_auth_url == "" { + t.Skipf("skipping, OS_AUTH_URL and friends must be set") + } + + if _, err := http.Get(os_auth_url); err != nil { + t.Skipf("skipping, unable to reach %s: %s", os_auth_url, err) + } + + client, err := swiftFactory(map[string]string{ + "path": "swift_test", + }) + if err != nil { + t.Fatalf("bad: %s", err) + } + + testClient(t, client) +} diff --git a/terraform/graphnodeconfigtype_string.go b/terraform/graphnodeconfigtype_string.go index de24c2dcd..d8c1724f4 100644 --- a/terraform/graphnodeconfigtype_string.go +++ b/terraform/graphnodeconfigtype_string.go @@ -9,7 +9,7 @@ const _GraphNodeConfigType_name = "GraphNodeConfigTypeInvalidGraphNodeConfigType var _GraphNodeConfigType_index = [...]uint8{0, 26, 53, 80, 105, 130, 157} func (i GraphNodeConfigType) String() string { - if i < 0 || i+1 >= GraphNodeConfigType(len(_GraphNodeConfigType_index)) { + if i < 0 || i >= GraphNodeConfigType(len(_GraphNodeConfigType_index)-1) { return fmt.Sprintf("GraphNodeConfigType(%d)", i) } return _GraphNodeConfigType_name[_GraphNodeConfigType_index[i]:_GraphNodeConfigType_index[i+1]] diff --git a/terraform/instancetype_string.go b/terraform/instancetype_string.go index fc8697644..3114bc157 100644 --- a/terraform/instancetype_string.go +++ b/terraform/instancetype_string.go @@ -9,7 +9,7 @@ const _InstanceType_name = "TypeInvalidTypePrimaryTypeTaintedTypeDeposed" var _InstanceType_index = [...]uint8{0, 11, 22, 33, 44} func (i InstanceType) String() string { - if i < 0 || i+1 >= InstanceType(len(_InstanceType_index)) { + if i < 0 || i >= InstanceType(len(_InstanceType_index)-1) { return fmt.Sprintf("InstanceType(%d)", i) } return _InstanceType_name[_InstanceType_index[i]:_InstanceType_index[i+1]] diff --git a/terraform/walkoperation_string.go b/terraform/walkoperation_string.go index ddd894f53..423793c3c 100644 --- a/terraform/walkoperation_string.go +++ b/terraform/walkoperation_string.go @@ -9,7 +9,7 @@ const _walkOperation_name = "walkInvalidwalkInputwalkApplywalkPlanwalkPlanDestro var _walkOperation_index = [...]uint8{0, 11, 20, 29, 37, 52, 63, 75} func (i walkOperation) String() string { - if i+1 >= walkOperation(len(_walkOperation_index)) { + if i >= walkOperation(len(_walkOperation_index)-1) { return fmt.Sprintf("walkOperation(%d)", i) } return _walkOperation_name[_walkOperation_index[i]:_walkOperation_index[i+1]] diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index 48a92b0a7..0cf800715 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -7,22 +7,23 @@ body.page-sub{ } body.layout-atlas, -body.layout-consul, -body.layout-dnsimple, -body.layout-dme, -body.layout-docker, +body.layout-aws, +body.layout-azure, body.layout-cloudflare, body.layout-cloudstack, +body.layout-consul, +body.layout-digitalocean, +body.layout-dme, +body.layout-dnsimple, +body.layout-docker, body.layout-google, body.layout-heroku, body.layout-mailgun, body.layout-openstack, body.layout-template, -body.layout-digitalocean, -body.layout-aws, body.layout-docs, -body.layout-inner, body.layout-downloads, +body.layout-inner, body.layout-intro{ background: $light-black image-url('sidebar-wire.png') left 62px no-repeat; @@ -287,4 +288,3 @@ body.layout-intro{ } } } - diff --git a/website/source/docs/commands/output.html.markdown b/website/source/docs/commands/output.html.markdown index ac1ab5a23..f1a70394e 100644 --- a/website/source/docs/commands/output.html.markdown +++ b/website/source/docs/commands/output.html.markdown @@ -21,4 +21,8 @@ current directory for the state file to query. The command-line flags are all optional. The list of available flags are: * `-state=path` - Path to the state file. Defaults to "terraform.tfstate". - +* `-module=module_name` - The module path which has needed output. + By default this is the root path. Other modules can be specified by + a period-separated list. Example: "foo" would reference the module + "foo" but "foo.bar" would reference the "bar" module in the "foo" + module. diff --git a/website/source/docs/providers/atlas/r/artifact.html.markdown b/website/source/docs/providers/atlas/r/artifact.html.markdown index 889413f21..3dee419f7 100644 --- a/website/source/docs/providers/atlas/r/artifact.html.markdown +++ b/website/source/docs/providers/atlas/r/artifact.html.markdown @@ -24,7 +24,7 @@ to this artifact will trigger a change to that instance. # Read the AMI resource "atlas_artifact" "web" { name = "hashicorp/web" - type = "aws.ami" + type = "amazon.ami" build = "latest" metadata { arch = "386" diff --git a/website/source/docs/providers/aws/r/autoscaling_notification.html.markdown b/website/source/docs/providers/aws/r/autoscaling_notification.html.markdown new file mode 100644 index 000000000..e47d337c6 --- /dev/null +++ b/website/source/docs/providers/aws/r/autoscaling_notification.html.markdown @@ -0,0 +1,68 @@ +--- +layout: "aws" +page_title: "AWS: aws_autoscaling_notification" +sidebar_current: "docs-aws-resource-autoscaling-notification" +description: |- + Provides an AutoScaling Group with Notification support +--- + +# aws\_autoscaling\_notification + +Provides an AutoScaling Group with Notification support, via SNS Topics. Each of +the `notifications` map to a [Notification Configuration][2] inside Amazon Web +Services, and are applied to each AutoScaling Group you supply. + +## Example Usage + +Basic usage: + +``` +resource "aws_autoscaling_notification" "example_notifications" { + group_names = [ + "${aws_autoscaling_group.bar.name}", + "${aws_autoscaling_group.foo.name}", + ] + notifications = [ + "autoscaling:EC2_INSTANCE_LAUNCH", + "autoscaling:EC2_INSTANCE_TERMINATE", + "autoscaling:EC2_INSTANCE_LAUNCH_ERROR" + ] + topic_arn = "${aws_sns_topic.example.arn}" +} + +resource "aws_sns_topic" "example" { + name = "example-topic" + # arn is an exported attribute +} + +resource "aws_autoscaling_group" "bar" { + name = "foobar1-terraform-test" + [... ASG attributes ...] +} + +resource "aws_autoscaling_group" "foo" { + name = "barfoo-terraform-test" + [... ASG attributes ...] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `group_names` - (Required) A list of AutoScaling Group Names +* `notifications` - (Required) A list of Notification Types that trigger +notifications. Acceptable are documented [in the AWS documentation here][1] +* `topic_arn` - (Required) The Topic ARN for notifications to be sent through + +## Attributes Reference + +The following attributes are exported: + +* `group_names` +* `notifications` +* `topic_arn` + + +[1]: http://docs.aws.amazon.com/AutoScaling/latest/APIReference/API_NotificationConfiguration.html +[2]: http://docs.aws.amazon.com/AutoScaling/latest/APIReference/API_DescribeNotificationConfigurations.html diff --git a/website/source/docs/providers/aws/r/db_instance.html.markdown b/website/source/docs/providers/aws/r/db_instance.html.markdown index def96d442..6d02d02b1 100644 --- a/website/source/docs/providers/aws/r/db_instance.html.markdown +++ b/website/source/docs/providers/aws/r/db_instance.html.markdown @@ -57,6 +57,8 @@ The following arguments are supported: * `iops` - (Optional) The amount of provisioned IOPS. Setting this implies a storage_type of "io1". * `maintenance_window` - (Optional) The window to perform maintenance in. + Syntax: "ddd:hh24:mi-ddd:hh24:mi". Eg: "Mon:00:00-Mon:03:00". + See [RDS Maintenance Window docs](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/AdjustingTheMaintenanceWindow.html) for more. * `multi_az` - (Optional) Specifies if the RDS instance is multi-AZ * `port` - (Optional) The port on which the DB accepts connections. * `publicly_accessible` - (Optional) Bool to control if instance is publicly accessible. diff --git a/website/source/docs/providers/aws/r/ecs_cluster.html.markdown b/website/source/docs/providers/aws/r/ecs_cluster.html.markdown new file mode 100644 index 000000000..d00e05fab --- /dev/null +++ b/website/source/docs/providers/aws/r/ecs_cluster.html.markdown @@ -0,0 +1,32 @@ +--- +layout: "aws" +page_title: "AWS: aws_ecs_cluster" +sidebar_current: "docs-aws-resource-ecs-cluster" +description: |- + Provides an ECS cluster. +--- + +# aws\_ecs\_cluster + +Provides an ECS cluster. + +## Example Usage + +``` +resource "aws_ecs_cluster" "foo" { + name = "white-hart" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the cluster (up to 255 letters, numbers, hyphens, and underscores) + +## Attributes Reference + +The following attributes are exported: + +* `name` - The name of the cluster +* `id` - The Amazon Resource Name (ARN) that identifies the cluster diff --git a/website/source/docs/providers/aws/r/ecs_service.html.markdown b/website/source/docs/providers/aws/r/ecs_service.html.markdown new file mode 100644 index 000000000..cd45b788a --- /dev/null +++ b/website/source/docs/providers/aws/r/ecs_service.html.markdown @@ -0,0 +1,59 @@ +--- +layout: "aws" +page_title: "AWS: aws_ecs_service" +sidebar_current: "docs-aws-resource-ecs-service" +description: |- + Provides an ECS service. +--- + +# aws\_ecs\_service + +Provides an ECS service - effectively a task that is expected to run until an error occures or user terminates it (typically a webserver or a database). + +See [ECS Services section in AWS developer guide](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_services.html). + +## Example Usage + +``` +resource "aws_ecs_service" "mongo" { + name = "mongodb" + cluster = "${aws_ecs_cluster.foo.id}" + task_definition = "${aws_ecs_task_definition.mongo.arn}" + desired_count = 3 + iam_role = "${aws_iam.foo.id}" + + load_balancer { + elb_name = "${aws_elb.foo.id}" + container_name = "mongo" + container_port = 8080 + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the service (up to 255 letters, numbers, hyphens, and underscores) +* `task_definition` - (Required) The family and revision (`family:revision`) or full ARN of the task definition that you want to run in your service. +* `desired_count` - (Required) The number of instances of the task definition to place and keep running +* `cluster` - (Optional) ARN of an ECS cluster +* `iam_role` - (Optional) IAM role that allows your Amazon ECS container agent to make calls to your load balancer on your behalf. This parameter is only required if you are using a load balancer with your service. +* `load_balancer` - (Optional) A load balancer block. Load balancers documented below. + +Load balancers support the following: + +* `elb_name` - (Required) The name of the load balancer. +* `container_name` - (Required) The name of the container to associate with the load balancer (as it appears in a container definition). +* `container_port` - (Required) The port on the container to associate with the load balancer. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - The Amazon Resource Name (ARN) that identifies the service +* `name` - The name of the service +* `cluster` - The Amazon Resource Name (ARN) of cluster which the service runs on +* `iam_role` - The ARN of IAM role used for ELB +* `desired_count` - The number of instances of the task definition diff --git a/website/source/docs/providers/aws/r/ecs_task_definition.html.markdown b/website/source/docs/providers/aws/r/ecs_task_definition.html.markdown new file mode 100644 index 000000000..0a265af4e --- /dev/null +++ b/website/source/docs/providers/aws/r/ecs_task_definition.html.markdown @@ -0,0 +1,50 @@ +--- +layout: "aws" +page_title: "AWS: aws_ecs_task_definition" +sidebar_current: "docs-aws-resource-ecs-task-definition" +description: |- + Provides an ECS task definition. +--- + +# aws\_ecs\_task\_definition + +Provides an ECS task definition to be used in `aws_ecs_service`. + +~> **NOTE:** There is currently no way to unregister +any previously registered task definition. +See related [thread in AWS forum](https://forums.aws.amazon.com/thread.jspa?threadID=170378&tstart=0). + +## Example Usage + +``` +resource "aws_ecs_task_definition" "jenkins" { + family = "jenkins" + container_definitions = "${file("task-definitions/jenkins.json")}" + + volume { + name = "jenkins-home" + host_path = "/ecs/jenkins-home" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `family` - (Required) The family, unique name for your task definition. +* `container_definitions` - (Required) A list of container definitions in JSON format. See [AWS docs](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_defintions.html) for syntax. +* `volume` - (Optional) A volume block. Volumes documented below. + +Volumes support the following: + +* `name` - (Required) The name of the volume. This name is referenced in the `sourceVolume` parameter of container definition `mountPoints`. +* `host_path` - (Required) The path on the host container instance that is presented to the container. + +## Attributes Reference + +The following attributes are exported: + +* `arn` - Full ARN of the task definition (including both `family` & `revision`) +* `family` - The family of the task definition. +* `revision` - The revision of the task in a particular family. diff --git a/website/source/docs/providers/aws/r/elasticache_cluster.html.markdown b/website/source/docs/providers/aws/r/elasticache_cluster.html.markdown index ccc4429dd..60f91b88a 100644 --- a/website/source/docs/providers/aws/r/elasticache_cluster.html.markdown +++ b/website/source/docs/providers/aws/r/elasticache_cluster.html.markdown @@ -1,6 +1,6 @@ --- layout: "aws" -page_title: "AWS: aws_subnet" +page_title: "AWS: aws_elasticache_cluster" sidebar_current: "docs-aws-resource-elasticache-cluster" description: |- Provides an VPC subnet resource. @@ -17,6 +17,7 @@ resource "aws_elasticache_cluster" "bar" { cluster_id = "cluster-example" engine = "memcached" node_type = "cache.m1.small" + port = 11211 num_cache_nodes = 1 parameter_group_name = "default.memcached1.4" } @@ -47,8 +48,8 @@ value must be between 1 and 20 * `parameter_group_name` – (Required) Name of the parameter group to associate with this cache cluster -* `port` – (Optional) The port number on which each of the cache nodes will -accept connections. Default 11211. +* `port` – (Required) The port number on which each of the cache nodes will +accept connections. For Memcache the default is 11211, and for Redis the default port is 6379. * `subnet_group_name` – (Optional, VPC only) Name of the subnet group to be used for the cache cluster. @@ -59,6 +60,8 @@ names to associate with this cache cluster * `security_group_ids` – (Optional, VPC only) One or more VPC security groups associated with the cache cluster +* `tags` - (Optional) A mapping of tags to assign to the resource. + ## Attributes Reference diff --git a/website/source/docs/providers/aws/r/elb.html.markdown b/website/source/docs/providers/aws/r/elb.html.markdown index 1f4a0b507..9c63788d3 100644 --- a/website/source/docs/providers/aws/r/elb.html.markdown +++ b/website/source/docs/providers/aws/r/elb.html.markdown @@ -46,6 +46,10 @@ resource "aws_elb" "bar" { idle_timeout = 400 connection_draining = true connection_draining_timeout = 400 + + tags { + Name = "foobar-terraform-elb" + } } ``` @@ -65,6 +69,7 @@ The following arguments are supported: * `idle_timeout` - (Optional) The time in seconds that the connection is allowed to be idle. Default: 60. * `connection_draining` - (Optional) Boolean to enable connection draining. * `connection_draining_timeout` - (Optional) The time in seconds to allow for connections to drain. +* `tags` - (Optional) A mapping of tags to assign to the resource. Exactly one of `availability_zones` or `subnets` must be specified: this determines if the ELB exists in a VPC or in EC2-classic. diff --git a/website/source/docs/providers/aws/r/network_interface.markdown b/website/source/docs/providers/aws/r/network_interface.markdown new file mode 100644 index 000000000..8144d8f0f --- /dev/null +++ b/website/source/docs/providers/aws/r/network_interface.markdown @@ -0,0 +1,51 @@ +--- +layout: "aws" +page_title: "AWS: aws_network_interface" +sidebar_current: "docs-aws-resource-network-interface" +description: |- + Provides an Elastic network interface (ENI) resource. +--- + +# aws\_network\_interface + +Provides an Elastic network interface (ENI) resource. + +## Example Usage + +``` +resource "aws_network_interface" "test" { + subnet_id = "${aws_subnet.public_a.id}" + private_ips = ["10.0.0.50"] + security_groups = ["${aws_security_group.web.name}"] + attachment { + instance = "${aws_instance.test.id}" + device_index = 1 + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `subnet_id` - (Required) Subnet ID to create the ENI in. +* `private_ips` - (Optional) List of private IPs to assign to the ENI. +* `security_groups` - (Optional) List of security group IDs to assign to the ENI. +* `attachment` - (Required) Block to define the attachment of the ENI. Documented below. +* `tags` - (Optional) A mapping of tags to assign to the resource. + +The `attachment` block supports: + +* `instance` - (Required) ID of the instance to attach to. +* `device_index` - (Required) Integer to define the devices index. + +## Attributes Reference + +The following attributes are exported: + +* `subnet_id` - Subnet ID the ENI is in. +* `private_ips` - List of private IPs assigned to the ENI. +* `security_groups` - List of security groups attached to the ENI. +* `attachment` - Block defining the attachment of the ENI. +* `tags` - Tags assigned to the ENI. + diff --git a/website/source/docs/providers/aws/r/s3_bucket.html.markdown b/website/source/docs/providers/aws/r/s3_bucket.html.markdown index c0ecd91c7..01a6a47b5 100644 --- a/website/source/docs/providers/aws/r/s3_bucket.html.markdown +++ b/website/source/docs/providers/aws/r/s3_bucket.html.markdown @@ -32,7 +32,7 @@ resource "aws_s3_bucket" "b" { resource "aws_s3_bucket" "b" { bucket = "s3-website-test.hashicorp.com" acl = "public-read" - policy = "#{file("policy.json")}" + policy = "${file("policy.json")}" website { index_document = "index.html" diff --git a/website/source/docs/providers/aws/r/security_group.html.markdown b/website/source/docs/providers/aws/r/security_group.html.markdown index c3f74dba5..ebd21bc73 100644 --- a/website/source/docs/providers/aws/r/security_group.html.markdown +++ b/website/source/docs/providers/aws/r/security_group.html.markdown @@ -28,7 +28,7 @@ resource "aws_security_group" "allow_all" { ingress { from_port = 0 - to_port = 65535 + to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } diff --git a/website/source/docs/providers/aws/r/sqs_queue.html.markdown b/website/source/docs/providers/aws/r/sqs_queue.html.markdown index 47cae1f7a..a3bf25c57 100644 --- a/website/source/docs/providers/aws/r/sqs_queue.html.markdown +++ b/website/source/docs/providers/aws/r/sqs_queue.html.markdown @@ -23,6 +23,7 @@ resource "aws_sqs_queue" "terraform_queue" { ## Argument Reference The following arguments are supported: + * `name` - (Required) This is the human-readable name of the queue * `visibility_timeout_seconds` - (Optional) The time in seconds that the delivery of all messages in the queue will be delayed. An integer from 0 to 900 (15 minutes). The default for this attribute is 30 seconds * `message_retention_seconds` - (Optional) The number of seconds Amazon SQS retains a message. Integer representing seconds, from 60 (1 minute) to 1209600 (14 days). The default for this attribute is 345600 (4 days). @@ -36,4 +37,4 @@ The following arguments are supported: The following attributes are exported: * `id` - The URL for the created Amazon SQS queue. -* `arn` - The ARN of the SQS queue \ No newline at end of file +* `arn` - The ARN of the SQS queue diff --git a/website/source/docs/providers/aws/r/vpc_dhcp_options.html.markdown b/website/source/docs/providers/aws/r/vpc_dhcp_options.html.markdown index 3e595adcc..2fc2727f2 100644 --- a/website/source/docs/providers/aws/r/vpc_dhcp_options.html.markdown +++ b/website/source/docs/providers/aws/r/vpc_dhcp_options.html.markdown @@ -60,7 +60,7 @@ The following attributes are exported: * `id` - The ID of the DHCP Options Set. ## Known Issues -* https://github.com/awslabs/aws-sdk-go/issues/210 +* https://github.com/aws/aws-sdk-go/issues/210 You can find more technical documentation about DHCP Options Set in the official [AWS User Guide](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_DHCP_Options.html). diff --git a/website/source/docs/providers/azure/index.html.markdown b/website/source/docs/providers/azure/index.html.markdown new file mode 100644 index 000000000..ea38e472e --- /dev/null +++ b/website/source/docs/providers/azure/index.html.markdown @@ -0,0 +1,48 @@ +--- +layout: "azure" +page_title: "Provider: Azure" +sidebar_current: "docs-azure-index" +description: |- + The Azure provider is used to interact with the many resources supported by Azure. The provider needs to be configured with a publish settings file and optionally a subscription ID before it can be used. +--- + +# Azure Provider + +The Azure provider is used to interact with the many resources supported +by Azure. The provider needs to be configured with a [publish settings +file](https://manage.windowsazure.com/publishsettings) and optionally a +subscription ID before it can be used. + +Use the navigation to the left to read about the available resources. + +## Example Usage + +``` +# Configure the Azure Provider +provider "azure" { + settings_file = "${var.azure_settings_file}" +} + +# Create a web server +resource "azure_instance" "web" { + ... +} +``` + +## Argument Reference + +The following arguments are supported: + +* `settings_file` - (Optional) The path to a publish settings file used to + authenticate with the Azure API. You can download the settings file here: + https://manage.windowsazure.com/publishsettings. You must either provide + (or source from the `AZURE_SETTINGS_FILE` environment variable) a settings + file or both a `subscription_id` and `certificate`. + +* `subscription_id` - (Optional) The subscription ID to use. If a + `settings_file` is not provided `subscription_id` is required. It can also + be sourced from the `AZURE_SUBSCRIPTION_ID` environment variable. + +* `certificate` - (Optional) The certificate used to authenticate with the + Azure API. If a `settings_file` is not provided `certificate` is required. + It can also be sourced from the `AZURE_CERTIFICATE` environment variable. diff --git a/website/source/docs/providers/azure/r/data_disk.html.markdown b/website/source/docs/providers/azure/r/data_disk.html.markdown new file mode 100644 index 000000000..3e95c5736 --- /dev/null +++ b/website/source/docs/providers/azure/r/data_disk.html.markdown @@ -0,0 +1,70 @@ +--- +layout: "azure" +page_title: "Azure: azure_data_disk" +sidebar_current: "docs-azure-resource-data-disk" +description: |- + Adds a data disk to a virtual machine. If the name of an existing disk is given, it will attach that disk. Otherwise it will create and attach a new empty disk. +--- + +# azure\_data\_disk + +Adds a data disk to a virtual machine. If the name of an existing disk is given, +it will attach that disk. Otherwise it will create and attach a new empty disk. + +## Example Usage + +``` +resource "azure_data_disk" "data" { + lun = 0 + size = 10 + storage = "yourstorage" + virtual_machine = "server1" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Optional) The name of an existing registered disk to attach to the + virtual machine. If left empty, a new empty disk will be created and + attached instead. Changing this forces a new resource to be created. + +* `label` - (Optional) The identifier of the data disk. Changing this forces a + new resource to be created (defaults to "virtual_machine-lun") + +* `lun` - (Required) The Logical Unit Number (LUN) for the disk. The LUN + specifies the slot in which the data drive appears when mounted for usage + by the virtual machine. Valid LUN values are 0 through 31. + +* `size` - (Optional) The size, in GB, of an empty disk to be attached to the + virtual machine. Required when creating a new disk, not used otherwise. + +* `caching` - (Optional) The caching behavior of data disk. Valid options are: + `None`, `ReadOnly` and `ReadWrite` (defaults `None`) + +* `storage ` - (Optional) The name of an existing storage account within the + subscription which will be used to store the VHD of this disk. Required + if no value is supplied for `media_link`. Changing this forces a new + resource to be created. + +* `media_link` - (Optional) The location of the blob in storage where the VHD + of this disk will be created. The storage account where must be associated + with the subscription. Changing this forces a new resource to be created. + +* `source_media_link` - (Optional) The location of a blob in storage where a + VHD file is located that is imported and registered as a disk. If a value + is supplied, `media_link` will not be used. + +* `virtual_machine` - (Required) The name of the virtual machine the disk will + be attached to. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The security group ID. +* `name` - The name of the disk. +* `label` - The identifier for the disk. +* `media_link` - The location of the blob in storage where the VHD of this disk + is created. diff --git a/website/source/docs/providers/azure/r/instance.html.markdown b/website/source/docs/providers/azure/r/instance.html.markdown new file mode 100644 index 000000000..0ece7668d --- /dev/null +++ b/website/source/docs/providers/azure/r/instance.html.markdown @@ -0,0 +1,119 @@ +--- +layout: "azure" +page_title: "Azure: azure_instance" +sidebar_current: "docs-azure-resource-instance" +description: |- + Creates a hosted service, role and deployment and then creates a virtual machine in the deployment based on the specified configuration. +--- + +# azure\_instance + +Creates a hosted service, role and deployment and then creates a virtual +machine in the deployment based on the specified configuration. + +## Example Usage + +``` +resource "azure_instance" "web" { + name = "terraform-test" + image = "Ubuntu Server 14.04 LTS" + size = "Basic_A1" + storage = "yourstorage" + location = "West US" + username = "terraform" + password = "Pass!admin123" + + endpoint { + name = "SSH" + protocol = "tcp" + public_port = 22 + private_port = 22 + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the instance. Changing this forces a new + resource to be created. + +* `description` - (Optional) The description for the associated hosted service. + Changing this forces a new resource to be created (defaults to the instance + name). + +* `image` - (Required) The name of an existing VM or OS image to use for this + instance. Changing this forces a new resource to be created. + +* `size` - (Required) The size of the instance. + +* `subnet` - (Optional) The name of the subnet to connect this instance to. If + a value is supplied `virtual_network` is required. Changing this forces a + new resource to be created. + +* `virtual_network` - (Optional) The name of the virtual network the `subnet` + belongs to. If a value is supplied `subnet` is required. Changing this + forces a new resource to be created. + +* `storage` - (Optional) The name of an existing storage account within the + subscription which will be used to store the VHDs of this instance. + Changing this forces a new resource to be created. + +* `reverse_dns` - (Optional) The DNS address to which the IP address of the + hosted service resolves when queried using a reverse DNS query. Changing + this forces a new resource to be created. + +* `location` - (Required) The location/region where the cloud service is + created. Changing this forces a new resource to be created. + +* `automatic_updates` - (Optional) If true this will enable automatic updates. + This attribute is only used when creating a Windows instance. Changing this + forces a new resource to be created (defaults false) + +* `time_zone` - (Optional) The appropriate time zone for this instance in the + format 'America/Los_Angeles'. This attribute is only used when creating a + Windows instance. Changing this forces a new resource to be created + (defaults false) + +* `username` - (Required) The username of a new user that will be created while + creating the instance. Changing this forces a new resource to be created. + +* `password` - (Optional) The password of the new user that will be created + while creating the instance. Required when creating a Windows instance or + when not supplying an `ssh_key_thumbprint` while creating a Linux instance. + Changing this forces a new resource to be created. + +* `ssh_key_thumbprint` - (Optional) The SSH thumbprint of an existing SSH key + within the subscription. This attribute is only used when creating a Linux + instance. Changing this forces a new resource to be created. + +* `security_group` - (Optional) The Network Security Group to associate with + this instance. + +* `endpoint` - (Optional) Can be specified multiple times to define multiple + endpoints. Each `endpoint` block supports fields documented below. + +The `endpoint` block supports: + +* `name` - (Required) The name of the external endpoint. + +* `protocol` - (Optional) The transport protocol for the endpoint. Valid + options are: `tcp` and `udp` (defaults `tcp`) + +* `public_port` - (Required) The external port to use for the endpoint. + +* `private_port` - (Required) The private port on which the instance is + listening. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The instance ID. +* `description` - The description for the associated hosted service. +* `subnet` - The subnet the instance is connected to. +* `endpoint` - The complete set of configured endpoints. +* `security_group` - The associated Network Security Group. +* `ip_address` - The private IP address assigned to the instance. +* `vip_address` - The public IP address assigned to the instance. diff --git a/website/source/docs/providers/azure/r/security_group.markdown b/website/source/docs/providers/azure/r/security_group.markdown new file mode 100644 index 000000000..c4699f716 --- /dev/null +++ b/website/source/docs/providers/azure/r/security_group.markdown @@ -0,0 +1,84 @@ +--- +layout: "azure" +page_title: "Azure: azure_security_group" +sidebar_current: "docs-azure-resource-security-group" +description: |- + Creates a new network security group within the context of the specified subscription. +--- + +# azure\_security\_group + +Creates a new network security group within the context of the specified +subscription. + +## Example Usage + +``` +resource "azure_security_group" "web" { + name = "webservers" + location = "West US" + + rule { + name = "HTTPS" + priority = 101 + source_cidr = "*" + source_port = "*" + destination_cidr = "*" + destination_port = "443" + protocol = "TCP" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the security group. Changing this forces a + new resource to be created. + +* `label` - (Optional) The identifier for the security group. The label can be + up to 1024 characters long. Changing this forces a new resource to be + created (defaults to the security group name) + +* `location` - (Required) The location/region where the security group is + created. Changing this forces a new resource to be created. + +* `rule` - (Required) Can be specified multiple times to define multiple + rules. Each `rule` block supports fields documented below. + +The `rule` block supports: + +* `name` - (Required) The name of the security rule. + +* `type ` - (Optional) The type of the security rule. Valid options are: + `Inbound` and `Outbound` (defaults `Inbound`) + +* `priority` - (Required) The priority of the network security rule. Rules with + lower priority are evaluated first. This value can be between 100 and 4096. + +* `action` - (Optional) The action that is performed when the security rule is + matched. Valid options are: `Allow` and `Deny` (defaults `Allow`) + +* `source_cidr` - (Required) The CIDR or source IP range. An asterisk (\*) can + also be used to match all source IPs. + +* `source_port` - (Required) The source port or range. This value can be + between 0 and 65535. An asterisk (\*) can also be used to match all ports. + +* `destination_cidr` - (Required) The CIDR or destination IP range. An asterisk + (\*) can also be used to match all destination IPs. + +* `destination_port` - (Required) The destination port or range. This value can + be between 0 and 65535. An asterisk (\*) can also be used to match all + ports. + +* `protocol` - (Optional) The protocol of the security rule. Valid options are: + `TCP`, `UDP` and `*` (defaults `TCP`) + +## Attributes Reference + +The following attributes are exported: + +* `id` - The security group ID. +* `label` - The identifier for the security group. diff --git a/website/source/docs/providers/azure/r/virtual_network.html.markdown b/website/source/docs/providers/azure/r/virtual_network.html.markdown new file mode 100644 index 000000000..66140c73d --- /dev/null +++ b/website/source/docs/providers/azure/r/virtual_network.html.markdown @@ -0,0 +1,59 @@ +--- +layout: "azure" +page_title: "Azure: azure_virtual_network" +sidebar_current: "docs-azure-resource-virtual-network" +description: |- + Creates a new virtual network including any configured subnets. Each subnet can optionally be configured with a security group to be associated with the subnet. +--- + +# azure\_virtual\_network + +Creates a new virtual network including any configured subnets. Each subnet can +optionally be configured with a security group to be associated with the subnet. + +## Example Usage + +``` +resource "azure_virtual_network" "default" { + name = "test-network" + address_space = ["10.1.2.0/24"] + location = "West US" + + subnet { + name = "subnet1" + address_prefix = "10.1.2.0/25" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the virtual network. Changing this forces a + new resource to be created. + +* `address_space` - (Required) The address space that is used the virtual + network. You can supply more than one address space. Changing this forces + a new resource to be created. + +* `location` - (Required) The location/region where the virtual network is + created. Changing this forces a new resource to be created. + +* `subnet` - (Required) Can be specified multiple times to define multiple + subnets. Each `subnet` block supports fields documented below. + +The `subnet` block supports: + +* `name` - (Required) The name of the subnet. + +* `address_prefix` - (Required) The address prefix to use for the subnet. + +* `security_group` - (Optional) The Network Security Group to associate with + the subnet. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The virtual NetworkConfiguration ID. diff --git a/website/source/docs/providers/cloudstack/index.html.markdown b/website/source/docs/providers/cloudstack/index.html.markdown index 3fe42b98a..0738370b6 100644 --- a/website/source/docs/providers/cloudstack/index.html.markdown +++ b/website/source/docs/providers/cloudstack/index.html.markdown @@ -43,3 +43,8 @@ The following arguments are supported: * `secret_key` - (Required) This is the CloudStack secret key. It must be provided, but it can also be sourced from the `CLOUDSTACK_SECRET_KEY` environment variable. + +* `timeout` - (Optional) A value in seconds. This is the time allowed for Cloudstack + to complete each asynchronous job triggered. If unset, this can be sourced from the + `CLOUDSTACK_TIMEOUT` environment variable. Otherwise, this will default to 300 + seconds. diff --git a/website/source/docs/providers/cloudstack/r/instance.html.markdown b/website/source/docs/providers/cloudstack/r/instance.html.markdown index e09cd6c6e..403c64d4b 100644 --- a/website/source/docs/providers/cloudstack/r/instance.html.markdown +++ b/website/source/docs/providers/cloudstack/r/instance.html.markdown @@ -48,6 +48,8 @@ The following arguments are supported: * `user_data` - (Optional) The user data to provide when launching the instance. +* `keypair` - (Optional) The name of the SSH keypair that will be used to access this instance. + * `expunge` - (Optional) This determines if the instance is expunged when it is destroyed (defaults false) diff --git a/website/source/docs/providers/cloudstack/r/ssh_keypair.html.markdown b/website/source/docs/providers/cloudstack/r/ssh_keypair.html.markdown new file mode 100644 index 000000000..4ad95c702 --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/ssh_keypair.html.markdown @@ -0,0 +1,36 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_ssh_keypair" +sidebar_current: "docs-cloudstack-resource-ssh-keypair" +description: |- + Creates or registers an SSH keypair. +--- + +# cloudstack\_ssh\_keypair + +Creates or registers an SSH keypair. + +## Example Usage + +``` +resource "cloudstack_ssh_keypair" "myKey" { + name = "myKey" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name to give the SSH keypair. This is a unique value within a Cloudstack account. + +* `public_key` - (Optional) The full public key text of this keypair. If this is omitted, Cloudstack + will generate a new keypair. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The keypair ID. This is set to the keypair `name` argument. +* `fingerprint` - The fingerprint of the public key specified or calculated. +* `private_key` - This is returned only if Cloudstack generated the keypair. diff --git a/website/source/docs/providers/docker/r/container.html.markdown b/website/source/docs/providers/docker/r/container.html.markdown index d5a4c823e..8da348ec2 100644 --- a/website/source/docs/providers/docker/r/container.html.markdown +++ b/website/source/docs/providers/docker/r/container.html.markdown @@ -46,6 +46,7 @@ The following arguments are supported: kept running. If false, then as long as the container exists, Terraform assumes it is successful. * `ports` - (Optional) See [Ports](#ports) below for details. +* `privileged` - (Optional, bool) Run container in privileged mode. * `publish_all_ports` - (Optional, bool) Publish all ports of the container. * `volumes` - (Optional) See [Volumes](#volumes) below for details. diff --git a/website/source/docs/providers/openstack/index.html.markdown b/website/source/docs/providers/openstack/index.html.markdown index 808b71d1e..be918a465 100644 --- a/website/source/docs/providers/openstack/index.html.markdown +++ b/website/source/docs/providers/openstack/index.html.markdown @@ -60,6 +60,10 @@ The following arguments are supported: * `insecure` - (Optional) Explicitly allow the provider to perform "insecure" SSL requests. If omitted, default value is `false` +* `endpoint_type` - (Optional) Specify which type of endpoint to use from the + service catalog. It can be set using the OS_ENDPOINT_TYPE environment + variable. If not set, public endpoints is used. + ## Testing In order to run the Acceptance Tests for development, the following environment diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 219b3ed3c..cbb7eb1aa 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -17,6 +17,10 @@ aws_autoscaling_group + > + aws_autoscaling_notification + + > aws_customer_gateway @@ -41,6 +45,18 @@ aws_ebs_volume + > + aws_ecs_cluster + + + > + aws_ecs_service + + + > + aws_ecs_task_definition + + > aws_eip @@ -133,6 +149,10 @@ aws_network_acl + > + aws_network_acl + + > aws_proxy_protocol_policy diff --git a/website/source/layouts/azure.erb b/website/source/layouts/azure.erb new file mode 100644 index 000000000..4699b354d --- /dev/null +++ b/website/source/layouts/azure.erb @@ -0,0 +1,38 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> +<% end %> diff --git a/website/source/layouts/cloudstack.erb b/website/source/layouts/cloudstack.erb index d9830586c..c2051b1b7 100644 --- a/website/source/layouts/cloudstack.erb +++ b/website/source/layouts/cloudstack.erb @@ -53,6 +53,10 @@ cloudstack_port_forward + > + cloudstack_ssh_keypair + + > cloudstack_template diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index dd60cad04..dbbf87b8a 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -119,19 +119,23 @@