diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 350aeb75da..cfb54f54fd 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -207,6 +207,7 @@ func Provider() terraform.ResourceProvider { "aws_main_route_table_association": resourceAwsMainRouteTableAssociation(), "aws_network_acl": resourceAwsNetworkAcl(), "aws_network_interface": resourceAwsNetworkInterface(), + "aws_opsworks_stack": resourceAwsOpsworksStack(), "aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(), "aws_route53_delegation_set": resourceAwsRoute53DelegationSet(), "aws_route53_record": resourceAwsRoute53Record(), diff --git a/builtin/providers/aws/resource_aws_opsworks_stack.go b/builtin/providers/aws/resource_aws_opsworks_stack.go new file mode 100644 index 0000000000..5d0bf1aa83 --- /dev/null +++ b/builtin/providers/aws/resource_aws_opsworks_stack.go @@ -0,0 +1,456 @@ +package aws + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/opsworks" +) + +func resourceAwsOpsworksStack() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsOpsworksStackCreate, + Read: resourceAwsOpsworksStackRead, + Update: resourceAwsOpsworksStackUpdate, + Delete: resourceAwsOpsworksStackDelete, + + Schema: map[string]*schema.Schema{ + "id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "region": &schema.Schema{ + Type: schema.TypeString, + ForceNew: true, + Required: true, + }, + + "service_role_arn": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "default_instance_profile_arn": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "color": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "configuration_manager_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "Chef", + }, + + "configuration_manager_version": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "11.4", + }, + + "manage_berkshelf": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "berkshelf_version": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "3.2.0", + }, + + "custom_cookbooks_source": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "url": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "username": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "password": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "revision": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "ssh_key": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "custom_json": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "default_availability_zone": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "default_os": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "Ubuntu 12.04 LTS", + }, + + "default_root_device_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "instance-store", + }, + + "default_ssh_key_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "default_subnet_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "hostname_theme": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "Layer_Dependent", + }, + + "use_custom_cookbooks": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "use_opsworks_security_groups": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "vpc_id": &schema.Schema{ + Type: schema.TypeString, + ForceNew: true, + Optional: true, + }, + }, + } +} + +func resourceAwsOpsworksStackValidate(d *schema.ResourceData) error { + cookbooksSourceCount := d.Get("custom_cookbooks_source.#").(int) + if cookbooksSourceCount > 1 { + return fmt.Errorf("Only one custom_cookbooks_source is permitted") + } + + vpcId := d.Get("vpc_id").(string) + if vpcId != "" { + if d.Get("default_subnet_id").(string) == "" { + return fmt.Errorf("default_subnet_id must be set if vpc_id is set") + } + } else { + if d.Get("default_availability_zone").(string) == "" { + return fmt.Errorf("either vpc_id or default_availability_zone must be set") + } + } + + return nil +} + +func resourceAwsOpsworksStackCustomCookbooksSource(d *schema.ResourceData) *opsworks.Source { + count := d.Get("custom_cookbooks_source.#").(int) + if count == 0 { + return nil + } + + return &opsworks.Source{ + Type: aws.String(d.Get("custom_cookbooks_source.0.type").(string)), + Url: aws.String(d.Get("custom_cookbooks_source.0.url").(string)), + Username: aws.String(d.Get("custom_cookbooks_source.0.username").(string)), + Password: aws.String(d.Get("custom_cookbooks_source.0.password").(string)), + Revision: aws.String(d.Get("custom_cookbooks_source.0.revision").(string)), + SshKey: aws.String(d.Get("custom_cookbooks_source.0.ssh_key").(string)), + } +} + +func resourceAwsOpsworksSetStackCustomCookbooksSource(d *schema.ResourceData, v *opsworks.Source) { + nv := make([]interface{}, 0, 1) + if v != nil { + m := make(map[string]interface{}) + if v.Type != nil { + m["type"] = *v.Type + } + if v.Url != nil { + m["url"] = *v.Url + } + if v.Username != nil { + m["username"] = *v.Username + } + if v.Password != nil { + m["password"] = *v.Password + } + if v.Revision != nil { + m["revision"] = *v.Revision + } + if v.SshKey != nil { + m["ssh_key"] = *v.SshKey + } + nv = append(nv, m) + } + + err := d.Set("custom_cookbooks_source", nv) + if err != nil { + // should never happen + panic(err) + } +} + +func resourceAwsOpsworksStackRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*AWSClient).opsworksconn + + req := &opsworks.DescribeStacksInput{ + StackIds: []*string{ + aws.String(d.Id()), + }, + } + + log.Printf("[DEBUG] Reading OpsWorks stack: %s", d.Id()) + + resp, err := client.DescribeStacks(req) + if err != nil { + if awserr, ok := err.(awserr.Error); ok { + if awserr.Code() == "ResourceNotFoundException" { + d.SetId("") + return nil + } + } + return err + } + + stack := resp.Stacks[0] + d.Set("name", stack.Name) + d.Set("region", stack.Region) + d.Set("default_instance_profile_arn", stack.DefaultInstanceProfileArn) + d.Set("service_role_arn", stack.ServiceRoleArn) + d.Set("default_availability_zone", stack.DefaultAvailabilityZone) + d.Set("default_os", stack.DefaultOs) + d.Set("default_root_device_type", stack.DefaultRootDeviceType) + d.Set("default_ssh_key_name", stack.DefaultSshKeyName) + d.Set("default_subnet_id", stack.DefaultSubnetId) + d.Set("hostname_theme", stack.HostnameTheme) + d.Set("use_custom_cookbooks", stack.UseCustomCookbooks) + d.Set("use_opsworks_security_groups", stack.UseOpsworksSecurityGroups) + d.Set("vpc_id", stack.VpcId) + if color, ok := stack.Attributes["Color"]; ok { + d.Set("color", color) + } + if stack.ConfigurationManager != nil { + d.Set("configuration_manager_name", stack.ConfigurationManager.Name) + d.Set("configuration_manager_version", stack.ConfigurationManager.Version) + } + if stack.ChefConfiguration != nil { + d.Set("berkshelf_version", stack.ChefConfiguration.BerkshelfVersion) + d.Set("manage_berkshelf", stack.ChefConfiguration.ManageBerkshelf) + } + resourceAwsOpsworksSetStackCustomCookbooksSource(d, stack.CustomCookbooksSource) + + return nil +} + +func resourceAwsOpsworksStackCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*AWSClient).opsworksconn + + err := resourceAwsOpsworksStackValidate(d) + if err != nil { + return err + } + + req := &opsworks.CreateStackInput{ + DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)), + Name: aws.String(d.Get("name").(string)), + Region: aws.String(d.Get("region").(string)), + ServiceRoleArn: aws.String(d.Get("service_role_arn").(string)), + } + inVpc := false + if vpcId, ok := d.GetOk("vpc_id"); ok { + req.VpcId = aws.String(vpcId.(string)) + inVpc = true + } + if defaultSubnetId, ok := d.GetOk("default_subnet_id"); ok { + req.DefaultSubnetId = aws.String(defaultSubnetId.(string)) + } + if defaultAvailabilityZone, ok := d.GetOk("default_availability_zone"); ok { + req.DefaultAvailabilityZone = aws.String(defaultAvailabilityZone.(string)) + } + + log.Printf("[DEBUG] Creating OpsWorks stack: %s", *req.Name) + + var resp *opsworks.CreateStackOutput + err = resource.Retry(20*time.Minute, func() error { + var cerr error + resp, cerr = client.CreateStack(req) + if cerr != nil { + if opserr, ok := cerr.(awserr.Error); ok { + // If Terraform is also managing the service IAM role, + // it may have just been created and not yet be + // propagated. + // AWS doesn't provide a machine-readable code for this + // specific error, so we're forced to do fragile message + // matching. + // The full error we're looking for looks something like + // the following: + // Service Role Arn: [...] is not yet propagated, please try again in a couple of minutes + if opserr.Code() == "ValidationException" && strings.Contains(opserr.Message(), "not yet propagated") { + log.Printf("[INFO] Waiting for service IAM role to propagate") + return cerr + } + } + return resource.RetryError{Err: cerr} + } + return nil + }) + if err != nil { + return err + } + + stackId := *resp.StackId + d.SetId(stackId) + d.Set("id", stackId) + + if inVpc { + // For VPC-based stacks, OpsWorks asynchronously creates some default + // security groups which must exist before layers can be created. + // Unfortunately it doesn't tell us what the ids of these are, so + // we can't actually check for them. Instead, we just wait a nominal + // amount of time for their creation to complete. + log.Print("[INFO] Waiting for OpsWorks built-in security groups to be created") + time.Sleep(30 * time.Second) + } + + return resourceAwsOpsworksStackUpdate(d, meta) +} + +func resourceAwsOpsworksStackUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*AWSClient).opsworksconn + + err := resourceAwsOpsworksStackValidate(d) + if err != nil { + return err + } + + req := &opsworks.UpdateStackInput{ + CustomJson: aws.String(d.Get("custom_json").(string)), + DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)), + DefaultRootDeviceType: aws.String(d.Get("default_root_device_type").(string)), + DefaultSshKeyName: aws.String(d.Get("default_ssh_key_name").(string)), + Name: aws.String(d.Get("name").(string)), + ServiceRoleArn: aws.String(d.Get("service_role_arn").(string)), + StackId: aws.String(d.Id()), + UseCustomCookbooks: aws.Bool(d.Get("use_custom_cookbooks").(bool)), + UseOpsworksSecurityGroups: aws.Bool(d.Get("use_opsworks_security_groups").(bool)), + Attributes: make(map[string]*string), + CustomCookbooksSource: resourceAwsOpsworksStackCustomCookbooksSource(d), + } + if v, ok := d.GetOk("default_os"); ok { + req.DefaultOs = aws.String(v.(string)) + } + if v, ok := d.GetOk("default_subnet_id"); ok { + req.DefaultSubnetId = aws.String(v.(string)) + } + if v, ok := d.GetOk("default_availability_zone"); ok { + req.DefaultAvailabilityZone = aws.String(v.(string)) + } + if v, ok := d.GetOk("hostname_theme"); ok { + req.HostnameTheme = aws.String(v.(string)) + } + if v, ok := d.GetOk("color"); ok { + req.Attributes["Color"] = aws.String(v.(string)) + } + req.ChefConfiguration = &opsworks.ChefConfiguration{ + BerkshelfVersion: aws.String(d.Get("berkshelf_version").(string)), + ManageBerkshelf: aws.Bool(d.Get("manage_berkshelf").(bool)), + } + req.ConfigurationManager = &opsworks.StackConfigurationManager{ + Name: aws.String(d.Get("configuration_manager_name").(string)), + Version: aws.String(d.Get("configuration_manager_version").(string)), + } + + log.Printf("[DEBUG] Updating OpsWorks stack: %s", d.Id()) + + _, err = client.UpdateStack(req) + if err != nil { + return err + } + + return resourceAwsOpsworksStackRead(d, meta) +} + +func resourceAwsOpsworksStackDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*AWSClient).opsworksconn + + req := &opsworks.DeleteStackInput{ + StackId: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Deleting OpsWorks stack: %s", d.Id()) + + _, err := client.DeleteStack(req) + if err != nil { + return err + } + + // For a stack in a VPC, OpsWorks has created some default security groups + // in the VPC, which it will now delete. + // Unfortunately, the security groups are deleted asynchronously and there + // is no robust way for us to determine when it is done. The VPC itself + // isn't deletable until the security groups are cleaned up, so this could + // make 'terraform destroy' fail if the VPC is also managed and we don't + // wait for the security groups to be deleted. + // There is no robust way to check for this, so we'll just wait a + // nominal amount of time. + if _, ok := d.GetOk("vpc_id"); ok { + log.Print("[INFO] Waiting for Opsworks built-in security groups to be deleted") + time.Sleep(30 * time.Second) + } + + return nil +} diff --git a/builtin/providers/aws/resource_aws_opsworks_stack_test.go b/builtin/providers/aws/resource_aws_opsworks_stack_test.go new file mode 100644 index 0000000000..b740b6a20d --- /dev/null +++ b/builtin/providers/aws/resource_aws_opsworks_stack_test.go @@ -0,0 +1,353 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/opsworks" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +// These tests assume the existence of predefined Opsworks IAM roles named `aws-opsworks-ec2-role` +// and `aws-opsworks-service-role`. + +/////////////////////////////// +//// Tests for the No-VPC case +/////////////////////////////// + +var testAccAwsOpsworksStackConfigNoVpcCreate = ` +resource "aws_opsworks_stack" "tf-acc" { + name = "tf-opsworks-acc" + region = "us-west-2" + service_role_arn = "%s" + default_instance_profile_arn = "%s" + default_availability_zone = "us-west-2a" + default_os = "Amazon Linux 2014.09" + default_root_device_type = "ebs" + custom_json = "{\"key\": \"value\"}" + configuration_manager_version = "11.10" + use_opsworks_security_groups = false +} +` +var testAccAWSOpsworksStackConfigNoVpcUpdate = ` +resource "aws_opsworks_stack" "tf-acc" { + name = "tf-opsworks-acc" + region = "us-west-2" + service_role_arn = "%s" + default_instance_profile_arn = "%s" + default_availability_zone = "us-west-2a" + default_os = "Amazon Linux 2014.09" + default_root_device_type = "ebs" + custom_json = "{\"key\": \"value\"}" + configuration_manager_version = "11.10" + use_opsworks_security_groups = false + use_custom_cookbooks = true + manage_berkshelf = true + custom_cookbooks_source { + type = "git" + revision = "master" + url = "https://github.com/awslabs/opsworks-example-cookbooks.git" + } +} +` + +func TestAccAwsOpsworksStackNoVpc(t *testing.T) { + opsiam := testAccAwsOpsworksStackIam{} + testAccAwsOpsworksStackPopulateIam(t, &opsiam) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsOpsworksStackDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: fmt.Sprintf(testAccAwsOpsworksStackConfigNoVpcCreate, opsiam.ServiceRoleArn, opsiam.InstanceProfileArn), + Check: testAccAwsOpsworksStackCheckResourceAttrsCreate, + }, + resource.TestStep{ + Config: fmt.Sprintf(testAccAWSOpsworksStackConfigNoVpcUpdate, opsiam.ServiceRoleArn, opsiam.InstanceProfileArn), + Check: testAccAwsOpsworksStackCheckResourceAttrsUpdate, + }, + }, + }) +} + +//////////////////////////// +//// Tests for the VPC case +//////////////////////////// + +var testAccAwsOpsworksStackConfigVpcCreate = ` +resource "aws_vpc" "tf-acc" { + cidr_block = "10.3.5.0/24" +} +resource "aws_subnet" "tf-acc" { + vpc_id = "${aws_vpc.tf-acc.id}" + cidr_block = "${aws_vpc.tf-acc.cidr_block}" + availability_zone = "us-west-2a" +} +resource "aws_opsworks_stack" "tf-acc" { + name = "tf-opsworks-acc" + region = "us-west-2" + vpc_id = "${aws_vpc.tf-acc.id}" + default_subnet_id = "${aws_subnet.tf-acc.id}" + service_role_arn = "%s" + default_instance_profile_arn = "%s" + default_os = "Amazon Linux 2014.09" + default_root_device_type = "ebs" + custom_json = "{\"key\": \"value\"}" + configuration_manager_version = "11.10" + use_opsworks_security_groups = false +} +` + +var testAccAWSOpsworksStackConfigVpcUpdate = ` +resource "aws_vpc" "tf-acc" { + cidr_block = "10.3.5.0/24" +} +resource "aws_subnet" "tf-acc" { + vpc_id = "${aws_vpc.tf-acc.id}" + cidr_block = "${aws_vpc.tf-acc.cidr_block}" + availability_zone = "us-west-2a" +} +resource "aws_opsworks_stack" "tf-acc" { + name = "tf-opsworks-acc" + region = "us-west-2" + vpc_id = "${aws_vpc.tf-acc.id}" + default_subnet_id = "${aws_subnet.tf-acc.id}" + service_role_arn = "%s" + default_instance_profile_arn = "%s" + default_os = "Amazon Linux 2014.09" + default_root_device_type = "ebs" + custom_json = "{\"key\": \"value\"}" + configuration_manager_version = "11.10" + use_opsworks_security_groups = false + use_custom_cookbooks = true + manage_berkshelf = true + custom_cookbooks_source { + type = "git" + revision = "master" + url = "https://github.com/awslabs/opsworks-example-cookbooks.git" + } +} +` + +func TestAccAwsOpsworksStackVpc(t *testing.T) { + opsiam := testAccAwsOpsworksStackIam{} + testAccAwsOpsworksStackPopulateIam(t, &opsiam) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsOpsworksStackDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: fmt.Sprintf(testAccAwsOpsworksStackConfigVpcCreate, opsiam.ServiceRoleArn, opsiam.InstanceProfileArn), + Check: testAccAwsOpsworksStackCheckResourceAttrsCreate, + }, + resource.TestStep{ + Config: fmt.Sprintf(testAccAWSOpsworksStackConfigVpcUpdate, opsiam.ServiceRoleArn, opsiam.InstanceProfileArn), + Check: resource.ComposeTestCheckFunc( + testAccAwsOpsworksStackCheckResourceAttrsUpdate, + testAccAwsOpsworksCheckVpc, + ), + }, + }, + }) +} + +//////////////////////////// +//// Checkers and Utilities +//////////////////////////// + +var testAccAwsOpsworksStackCheckResourceAttrsCreate = resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "name", + "tf-opsworks-acc", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "default_availability_zone", + "us-west-2a", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "default_os", + "Amazon Linux 2014.09", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "default_root_device_type", + "ebs", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "custom_json", + `{"key": "value"}`, + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "configuration_manager_version", + "11.10", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "use_opsworks_security_groups", + "false", + ), +) + +var testAccAwsOpsworksStackCheckResourceAttrsUpdate = resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "name", + "tf-opsworks-acc", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "default_availability_zone", + "us-west-2a", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "default_os", + "Amazon Linux 2014.09", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "default_root_device_type", + "ebs", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "custom_json", + `{"key": "value"}`, + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "configuration_manager_version", + "11.10", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "use_opsworks_security_groups", + "false", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "use_custom_cookbooks", + "true", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "manage_berkshelf", + "true", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "custom_cookbooks_source.0.type", + "git", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "custom_cookbooks_source.0.revision", + "master", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_stack.tf-acc", + "custom_cookbooks_source.0.url", + "https://github.com/awslabs/opsworks-example-cookbooks.git", + ), +) + +func testAccAwsOpsworksCheckVpc(s *terraform.State) error { + rs, ok := s.RootModule().Resources["aws_opsworks_stack.tf-acc"] + if !ok { + return fmt.Errorf("Not found: %s", "aws_opsworks_stack.tf-acc") + } + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + p := rs.Primary + + opsworksconn := testAccProvider.Meta().(*AWSClient).opsworksconn + describeOpts := &opsworks.DescribeStacksInput{ + StackIds: []*string{aws.String(p.ID)}, + } + resp, err := opsworksconn.DescribeStacks(describeOpts) + if err != nil { + return err + } + if len(resp.Stacks) == 0 { + return fmt.Errorf("No stack %s not found", p.ID) + } + if p.Attributes["vpc_id"] != *resp.Stacks[0].VpcId { + return fmt.Errorf("VPCID Got %s, expected %s", *resp.Stacks[0].VpcId, p.Attributes["vpc_id"]) + } + if p.Attributes["default_subnet_id"] != *resp.Stacks[0].DefaultSubnetId { + return fmt.Errorf("VPCID Got %s, expected %s", *resp.Stacks[0].DefaultSubnetId, p.Attributes["default_subnet_id"]) + } + return nil +} + +func testAccCheckAwsOpsworksStackDestroy(s *terraform.State) error { + if len(s.RootModule().Resources) > 0 { + return fmt.Errorf("Expected all resources to be gone, but found: %#v", s.RootModule().Resources) + } + + return nil +} + +// Holds the two IAM object ARNs used in stack objects we'll create. +type testAccAwsOpsworksStackIam struct { + ServiceRoleArn string + InstanceProfileArn string +} + +func testAccAwsOpsworksStackPopulateIam(t *testing.T, opsiam *testAccAwsOpsworksStackIam) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccInstanceConfig_pre, // noop + Check: testAccCheckAwsOpsworksEnsureIam(t, opsiam), + }, + }, + }) +} + +func testAccCheckAwsOpsworksEnsureIam(t *testing.T, opsiam *testAccAwsOpsworksStackIam) func(*terraform.State) error { + return func(_ *terraform.State) error { + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + + serviceRoleOpts := &iam.GetRoleInput{ + RoleName: aws.String("aws-opsworks-service-role"), + } + respServiceRole, err := iamconn.GetRole(serviceRoleOpts) + if err != nil { + return err + } + + instanceProfileOpts := &iam.GetInstanceProfileInput{ + InstanceProfileName: aws.String("aws-opsworks-ec2-role"), + } + respInstanceProfile, err := iamconn.GetInstanceProfile(instanceProfileOpts) + if err != nil { + return err + } + + opsiam.ServiceRoleArn = *respServiceRole.Role.Arn + opsiam.InstanceProfileArn = *respInstanceProfile.InstanceProfile.Arn + + t.Logf("[DEBUG] ServiceRoleARN for OpsWorks: %s", opsiam.ServiceRoleArn) + t.Logf("[DEBUG] Instance Profile ARN for OpsWorks: %s", opsiam.InstanceProfileArn) + + return nil + + } +} diff --git a/website/source/docs/providers/aws/r/opsworks_stack.html.markdown b/website/source/docs/providers/aws/r/opsworks_stack.html.markdown new file mode 100644 index 0000000000..d664ca1a95 --- /dev/null +++ b/website/source/docs/providers/aws/r/opsworks_stack.html.markdown @@ -0,0 +1,68 @@ +--- +layout: "aws" +page_title: "AWS: aws_opsworks_stack" +sidebar_current: "docs-aws-resource-opsworks-stack" +description: |- + Provides an OpsWorks stack resource. +--- + +# aws\_opsworks\_stack + +Provides an OpsWorks stack resource. + +## Example Usage + +``` +resource "aws_opsworks_stack" "main" { + name = "awesome-stack" + region = "us-west-1" + service_role_arn = "${aws_iam_role.opsworks.arn}" + default_instance_profile_arn = "${aws_iam_instance_profile.opsworks.arn}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the stack. +* `region` - (Required) The name of the region where the stack will exist. +* `service_role_arn` - (Required) The ARN of an IAM role that the OpsWorks service will act as. +* `default_instance_profile_arn` - (Required) The ARN of an IAM Instance Profile that created instances + will have by default. +* `berkshelf_version` - (Optional) If `manage_berkshelf` is enabled, the version of Berkshelf to use. +* `color` - (Optional) Color to paint next to the stack's resources in the OpsWorks console. +* `default_availability_zone` - (Optional) Name of the availability zone where instances will be created + by default. This is required unless you set `vpc_id`. +* `configuration_manager_name` - (Optional) Name of the configuration manager to use. Defaults to "Chef". +* `configuration_manager_version` - (Optional) Version of the configuratino manager to use. Defaults to "11.4". +* `custom_cookbooks_source` - (Optional) When `use_custom_cookbooks` is set, provide this sub-object as + described below. +* `default_os` - (Optional) Name of OS that will be installed on instances by default. +* `default_root_device_type` - (Optional) Name of the type of root device instances will have by default. +* `default_ssh_key_name` - (Optional) Name of the SSH keypair that instances will have by default. +* `default_subnet_id` - (Optional) Id of the subnet in which instances will be created by default. Mandatory + if `vpc_id` is set, and forbidden if it isn't. +* `hostname_theme` - (Optional) Keyword representing the naming scheme that will be used for instance hostnames + within this stack. +* `manage_berkshelf` - (Optional) Boolean value controlling whether Opsworks will run Berkshelf for this stack. +* `use_custom_cookbooks` - (Optional) Boolean value controlling whether the custom cookbook settings are + enabled. +* `use_opsworks_security_groups` - (Optional) Boolean value controlling whether the standard OpsWorks + security groups apply to created instances. +* `vpc_id` - (Optional) The id of the VPC that this stack belongs to. + +The `custom_cookbooks_source` block supports the following arguments: + +* `type` - (Required) The type of source to use. For example, "archive". +* `url` - (Required) The URL where the cookbooks resource can be found. +* `username` - (Optional) Username to use when authenticating to the source. +* `password` - (Optional) Password to use when authenticating to the source. +* `ssh_key` - (Optional) SSH key to use when authenticating to the source. +* `revision` - (Optional) For sources that are version-aware, the revision to use. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The id of the stack. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 08161f0b82..3996f817c4 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -259,6 +259,10 @@ OpsWorks Resources