diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index d9e0768411cd..aa4fcf2c17eb 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -85,7 +85,6 @@ func (c *Config) Client() (interface{}, error) { Credentials: creds, Region: "us-east-1", }) - } if len(errs) > 0 { diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 50596512e364..373f6819fa0a 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -52,6 +52,14 @@ func Provider() terraform.ResourceProvider { "aws_db_subnet_group": resourceAwsDbSubnetGroup(), "aws_eip": resourceAwsEip(), "aws_elb": resourceAwsElb(), + "aws_iam_access_key": resourceAwsIamAccessKey(), + "aws_iam_group_policy": resourceAwsIamGroupPolicy(), + "aws_iam_group": resourceAwsIamGroup(), + "aws_iam_instance_profile": resourceAwsIamInstanceProfile(), + "aws_iam_role_policy": resourceAwsIamRolePolicy(), + "aws_iam_role": resourceAwsIamRole(), + "aws_iam_user_policy": resourceAwsIamUserPolicy(), + "aws_iam_user": resourceAwsIamUser(), "aws_instance": resourceAwsInstance(), "aws_internet_gateway": resourceAwsInternetGateway(), "aws_key_pair": resourceAwsKeyPair(), @@ -61,8 +69,8 @@ func Provider() terraform.ResourceProvider { "aws_network_interface": resourceAwsNetworkInterface(), "aws_route53_record": resourceAwsRoute53Record(), "aws_route53_zone": resourceAwsRoute53Zone(), - "aws_route_table": resourceAwsRouteTable(), "aws_route_table_association": resourceAwsRouteTableAssociation(), + "aws_route_table": resourceAwsRouteTable(), "aws_s3_bucket": resourceAwsS3Bucket(), "aws_security_group": resourceAwsSecurityGroup(), "aws_subnet": resourceAwsSubnet(), diff --git a/builtin/providers/aws/resource_aws_autoscaling_group.go b/builtin/providers/aws/resource_aws_autoscaling_group.go index da8548e3fba9..99ddc5c2b74f 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group.go @@ -6,7 +6,6 @@ import ( "strings" "time" - "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" @@ -82,9 +81,7 @@ func resourceAwsAutoscalingGroup() *schema.Resource { Required: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, "load_balancers": &schema.Schema{ @@ -92,9 +89,7 @@ func resourceAwsAutoscalingGroup() *schema.Resource { Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, "vpc_zone_identifier": &schema.Schema{ @@ -103,9 +98,7 @@ func resourceAwsAutoscalingGroup() *schema.Resource { Computed: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, "termination_policies": &schema.Schema{ @@ -114,9 +107,7 @@ func resourceAwsAutoscalingGroup() *schema.Resource { Computed: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, "tag": autoscalingTagsSchema(), diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go index ed2da52ac6af..d4f37052baca 100644 --- a/builtin/providers/aws/resource_aws_db_instance.go +++ b/builtin/providers/aws/resource_aws_db_instance.go @@ -9,7 +9,6 @@ import ( "github.com/awslabs/aws-sdk-go/service/iam" "github.com/awslabs/aws-sdk-go/service/rds" - "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" ) @@ -132,18 +131,14 @@ func resourceAwsDbInstance() *schema.Resource { Optional: true, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, "security_group_names": &schema.Schema{ Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, "final_snapshot_identifier": &schema.Schema{ @@ -372,9 +367,7 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error { // Create an empty schema.Set to hold all vpc security group ids ids := &schema.Set{ - F: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + F: schema.HashString, } for _, v := range v.VPCSecurityGroups { ids.Add(*v.VPCSecurityGroupID) @@ -383,9 +376,7 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error { // Create an empty schema.Set to hold all security group names sgn := &schema.Set{ - F: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + F: schema.HashString, } for _, v := range v.DBSecurityGroups { sgn.Add(*v.DBSecurityGroupName) diff --git a/builtin/providers/aws/resource_aws_db_subnet_group.go b/builtin/providers/aws/resource_aws_db_subnet_group.go index e92350208415..0e82667549f5 100644 --- a/builtin/providers/aws/resource_aws_db_subnet_group.go +++ b/builtin/providers/aws/resource_aws_db_subnet_group.go @@ -8,7 +8,6 @@ import ( "github.com/awslabs/aws-sdk-go/aws" "github.com/awslabs/aws-sdk-go/service/rds" - "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" ) @@ -37,9 +36,7 @@ func resourceAwsDbSubnetGroup() *schema.Resource { Required: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, }, } diff --git a/builtin/providers/aws/resource_aws_elb.go b/builtin/providers/aws/resource_aws_elb.go index b36e752e9358..7f2d77f825d0 100644 --- a/builtin/providers/aws/resource_aws_elb.go +++ b/builtin/providers/aws/resource_aws_elb.go @@ -43,9 +43,7 @@ func resourceAwsElb() *schema.Resource { Optional: true, ForceNew: true, Computed: true, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, "instances": &schema.Schema{ @@ -53,9 +51,7 @@ func resourceAwsElb() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, Optional: true, Computed: true, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, // TODO: could be not ForceNew @@ -65,9 +61,7 @@ func resourceAwsElb() *schema.Resource { Optional: true, ForceNew: true, Computed: true, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, "subnets": &schema.Schema{ @@ -76,9 +70,7 @@ func resourceAwsElb() *schema.Resource { Optional: true, ForceNew: true, Computed: true, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, "listener": &schema.Schema{ diff --git a/builtin/providers/aws/resource_aws_iam_access_key.go b/builtin/providers/aws/resource_aws_iam_access_key.go new file mode 100644 index 000000000000..efeec62f723b --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_access_key.go @@ -0,0 +1,116 @@ +package aws + +import ( + "fmt" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsIamAccessKey() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsIamAccessKeyCreate, + Read: resourceAwsIamAccessKeyRead, + Delete: resourceAwsIamAccessKeyDelete, + + Schema: map[string]*schema.Schema{ + "user": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "status": &schema.Schema{ + Type: schema.TypeString, + // this could be settable, but goamz does not support the + // UpdateAccessKey API yet. + Computed: true, + }, + "secret": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsIamAccessKeyCreate(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.CreateAccessKeyInput{ + UserName: aws.String(d.Get("user").(string)), + } + + createResp, err := iamconn.CreateAccessKey(request) + if err != nil { + return fmt.Errorf( + "Error creating access key for user %s: %s", + *request.UserName, + err, + ) + } + + if err := d.Set("secret", createResp.AccessKey.SecretAccessKey); err != nil { + return err + } + return resourceAwsIamAccessKeyReadResult(d, &iam.AccessKeyMetadata{ + AccessKeyID: createResp.AccessKey.AccessKeyID, + CreateDate: createResp.AccessKey.CreateDate, + Status: createResp.AccessKey.Status, + UserName: createResp.AccessKey.UserName, + }) +} + +func resourceAwsIamAccessKeyRead(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.ListAccessKeysInput{ + UserName: aws.String(d.Get("user").(string)), + } + + getResp, err := iamconn.ListAccessKeys(request) + if err != nil { + if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX TEST ME + // the user does not exist, so the key can't exist. + d.SetId("") + return nil + } + return fmt.Errorf("Error reading IAM acces key: %s", err) + } + + for _, key := range getResp.AccessKeyMetadata { + if key.AccessKeyID != nil && *key.AccessKeyID == d.Id() { + return resourceAwsIamAccessKeyReadResult(d, key) + } + } + + // Guess the key isn't around anymore. + d.SetId("") + return nil +} + +func resourceAwsIamAccessKeyReadResult(d *schema.ResourceData, key *iam.AccessKeyMetadata) error { + d.SetId(*key.AccessKeyID) + if err := d.Set("user", key.UserName); err != nil { + return err + } + if err := d.Set("status", key.Status); err != nil { + return err + } + return nil +} + +func resourceAwsIamAccessKeyDelete(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.DeleteAccessKeyInput{ + AccessKeyID: aws.String(d.Id()), + UserName: aws.String(d.Get("user").(string)), + } + + if _, err := iamconn.DeleteAccessKey(request); err != nil { + return fmt.Errorf("Error deleting access key %s: %s", d.Id(), err) + } + return nil +} diff --git a/builtin/providers/aws/resource_aws_iam_access_key_test.go b/builtin/providers/aws/resource_aws_iam_access_key_test.go new file mode 100644 index 000000000000..c780e1b7e508 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_access_key_test.go @@ -0,0 +1,113 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSAccessKey_normal(t *testing.T) { + var conf iam.AccessKeyMetadata + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAccessKeyDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSAccessKeyConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAccessKeyExists("aws_iam_access_key.a_key", &conf), + testAccCheckAWSAccessKeyAttributes(&conf), + ), + }, + }, + }) +} + +func testAccCheckAWSAccessKeyDestroy(s *terraform.State) error { + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_access_key" { + continue + } + + // Try to get access key + resp, err := iamconn.ListAccessKeys(&iam.ListAccessKeysInput{ + UserName: aws.String(rs.Primary.ID), + }) + if err == nil { + if len(resp.AccessKeyMetadata) > 0 { + return fmt.Errorf("still exist.") + } + return nil + } + + // Verify the error is what we want + ec2err, ok := err.(aws.APIError) + if !ok { + return err + } + if ec2err.Code != "NoSuchEntity" { + return err + } + } + + return nil +} + +func testAccCheckAWSAccessKeyExists(n string, res *iam.AccessKeyMetadata) 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 Role name is set") + } + + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + + resp, err := iamconn.ListAccessKeys(&iam.ListAccessKeysInput{ + UserName: aws.String(rs.Primary.ID), + }) + if err != nil { + return err + } + + if len(resp.AccessKeyMetadata) != 1 || + *resp.AccessKeyMetadata[0].UserName != rs.Primary.ID { + return fmt.Errorf("User not found not found") + } + + *res = *resp.AccessKeyMetadata[0] + + return nil + } +} + +func testAccCheckAWSAccessKeyAttributes(accessKeyMetadata *iam.AccessKeyMetadata) resource.TestCheckFunc { + return func(s *terraform.State) error { + if *accessKeyMetadata.UserName != "some_usr" { + return fmt.Errorf("Bad username: %s", *accessKeyMetadata.UserName) + } + + if *accessKeyMetadata.Status != "Active" { + return fmt.Errorf("Bad status: %s", *accessKeyMetadata.Status) + } + + return nil + } +} + +const testAccAWSAccessKeyConfig = ` +resource "aws_iam_access_key" "a_key" { + user = "some_user" +} +` diff --git a/builtin/providers/aws/resource_aws_iam_group.go b/builtin/providers/aws/resource_aws_iam_group.go new file mode 100644 index 000000000000..1d0bc6cf4bc6 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_group.go @@ -0,0 +1,106 @@ +package aws + +import ( + "fmt" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsIamGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsIamGroupCreate, + Read: resourceAwsIamGroupRead, + // TODO + //Update: resourceAwsIamGroupUpdate, + Delete: resourceAwsIamGroupDelete, + + Schema: map[string]*schema.Schema{ + "arn": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "unique_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "path": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "/", + ForceNew: true, + }, + }, + } +} + +func resourceAwsIamGroupCreate(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + name := d.Get("name").(string) + + request := &iam.CreateGroupInput{ + Path: aws.String(d.Get("path").(string)), + GroupName: aws.String(name), + } + + createResp, err := iamconn.CreateGroup(request) + if err != nil { + return fmt.Errorf("Error creating IAM Group %s: %s", name, err) + } + return resourceAwsIamGroupReadResult(d, createResp.Group) +} + +func resourceAwsIamGroupRead(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.GetGroupInput{ + GroupName: aws.String(d.Id()), + } + + getResp, err := iamconn.GetGroup(request) + if err != nil { + if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { + d.SetId("") + return nil + } + return fmt.Errorf("Error reading IAM Group %s: %s", d.Id(), err) + } + return resourceAwsIamGroupReadResult(d, getResp.Group) +} + +func resourceAwsIamGroupReadResult(d *schema.ResourceData, group *iam.Group) error { + d.SetId(*group.GroupName) + if err := d.Set("name", group.GroupName); err != nil { + return err + } + if err := d.Set("arn", group.ARN); err != nil { + return err + } + if err := d.Set("path", group.Path); err != nil { + return err + } + if err := d.Set("unique_id", group.GroupID); err != nil { + return err + } + return nil +} + +func resourceAwsIamGroupDelete(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.DeleteGroupInput{ + GroupName: aws.String(d.Id()), + } + + if _, err := iamconn.DeleteGroup(request); err != nil { + return fmt.Errorf("Error deleting IAM Group %s: %s", d.Id(), err) + } + return nil +} diff --git a/builtin/providers/aws/resource_aws_iam_group_policy.go b/builtin/providers/aws/resource_aws_iam_group_policy.go new file mode 100644 index 000000000000..51190aa5fc32 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_group_policy.go @@ -0,0 +1,110 @@ +package aws + +import ( + "fmt" + "net/url" + "strings" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsIamGroupPolicy() *schema.Resource { + return &schema.Resource{ + // PutGroupPolicy API is idempotent, so these can be the same. + Create: resourceAwsIamGroupPolicyPut, + Update: resourceAwsIamGroupPolicyPut, + + Read: resourceAwsIamGroupPolicyRead, + Delete: resourceAwsIamGroupPolicyDelete, + + Schema: map[string]*schema.Schema{ + "policy": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "group": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsIamGroupPolicyPut(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.PutGroupPolicyInput{ + GroupName: aws.String(d.Get("group").(string)), + PolicyName: aws.String(d.Get("name").(string)), + PolicyDocument: aws.String(d.Get("policy").(string)), + } + + if _, err := iamconn.PutGroupPolicy(request); err != nil { + return fmt.Errorf("Error putting IAM group policy %s: %s", *request.PolicyName, err) + } + + d.SetId(fmt.Sprintf("%s:%s", *request.GroupName, *request.PolicyName)) + return nil +} + +func resourceAwsIamGroupPolicyRead(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + group, name := resourceAwsIamGroupPolicyParseId(d.Id()) + + request := &iam.GetGroupPolicyInput{ + PolicyName: aws.String(name), + GroupName: aws.String(group), + } + + getResp, err := iamconn.GetGroupPolicy(request) + if err != nil { + if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX test me + d.SetId("") + return nil + } + return fmt.Errorf("Error reading IAM policy %s from group %s: %s", name, group, err) + } + + if getResp.PolicyDocument == nil { + return fmt.Errorf("GetGroupPolicy returned a nil policy document") + } + + policy, err := url.QueryUnescape(*getResp.PolicyDocument) + if err != nil { + return err + } + return d.Set("policy", policy) +} + +func resourceAwsIamGroupPolicyDelete(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + group, name := resourceAwsIamGroupPolicyParseId(d.Id()) + + request := &iam.DeleteGroupPolicyInput{ + PolicyName: aws.String(name), + GroupName: aws.String(group), + } + + if _, err := iamconn.DeleteGroupPolicy(request); err != nil { + return fmt.Errorf("Error deleting IAM group policy %s: %s", d.Id(), err) + } + return nil +} + +func resourceAwsIamGroupPolicyParseId(id string) (groupName, policyName string) { + parts := strings.SplitN(id, ":", 2) + groupName = parts[0] + policyName = parts[1] + return +} diff --git a/builtin/providers/aws/resource_aws_iam_group_policy_test.go b/builtin/providers/aws/resource_aws_iam_group_policy_test.go new file mode 100644 index 000000000000..1778d52165a7 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_group_policy_test.go @@ -0,0 +1,112 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSIAMGroupPolicy(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckIAMGroupPolicyDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccIAMGroupPolicyConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMGroupPolicy( + "aws_iam_group.group", + "aws_iam_group_policy.foo", + ), + ), + }, + resource.TestStep{ + Config: testAccIAMGroupPolicyConfigUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMGroupPolicy( + "aws_iam_group.group", + "aws_iam_group_policy.bar", + ), + ), + }, + }, + }) +} + +func testAccCheckIAMGroupPolicyDestroy(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 +} + +func testAccCheckIAMGroupPolicy( + iamGroupResource string, + iamGroupPolicyResource string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[iamGroupResource] + if !ok { + return fmt.Errorf("Not Found: %s", iamGroupResource) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + policy, ok := s.RootModule().Resources[iamGroupPolicyResource] + if !ok { + return fmt.Errorf("Not Found: %s", iamGroupPolicyResource) + } + + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + group, name := resourceAwsIamGroupPolicyParseId(policy.Primary.ID) + _, err := iamconn.GetGroupPolicy(&iam.GetGroupPolicyInput{ + GroupName: aws.String(group), + PolicyName: aws.String(name), + }) + + if err != nil { + return err + } + + return nil + } +} + +const testAccIAMGroupPolicyConfig = ` +resource "aws_iam_group" "group" { + name = "test_group" + path = "/" +} + +resource "aws_iam_group_policy" "foo" { + name = "foo_policy" + group = "${aws_iam_group.group.name}" + policy = "{\"Version\":\"2012-10-17\",\"Statement\":{\"Effect\":\"Allow\",\"Action\":\"*\",\"Resource\":\"*\"}}" +} +` + +const testAccIAMGroupPolicyConfigUpdate = ` +resource "aws_iam_group" "group" { + name = "test_group" + path = "/" +} + +resource "aws_iam_group_policy" "foo" { + name = "foo_policy" + group = "${aws_iam_group.group.name}" + policy = "{\"Version\":\"2012-10-17\",\"Statement\":{\"Effect\":\"Allow\",\"Action\":\"*\",\"Resource\":\"*\"}}" +} + +resource "aws_iam_group_policy" "bar" { + name = "bar_policy" + group = "${aws_iam_group.group.name}" + policy = "{\"Version\":\"2012-10-17\",\"Statement\":{\"Effect\":\"Allow\",\"Action\":\"*\",\"Resource\":\"*\"}}" +} +` diff --git a/builtin/providers/aws/resource_aws_iam_group_test.go b/builtin/providers/aws/resource_aws_iam_group_test.go new file mode 100644 index 000000000000..b28f152fde98 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_group_test.go @@ -0,0 +1,106 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSGroup_normal(t *testing.T) { + var conf iam.GetGroupOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSGroupConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSGroupExists("aws_iam_group.group", &conf), + testAccCheckAWSGroupAttributes(&conf), + ), + }, + }, + }) +} + +func testAccCheckAWSGroupDestroy(s *terraform.State) error { + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_iam_group" { + continue + } + + // Try to get group + _, err := iamconn.GetGroup(&iam.GetGroupInput{ + GroupName: aws.String(rs.Primary.ID), + }) + if err == nil { + return fmt.Errorf("still exist.") + } + + // Verify the error is what we want + ec2err, ok := err.(aws.APIError) + if !ok { + return err + } + if ec2err.Code != "NoSuchEntity" { + return err + } + } + + return nil +} + +func testAccCheckAWSGroupExists(n string, res *iam.GetGroupOutput) 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 Group name is set") + } + + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + + resp, err := iamconn.GetGroup(&iam.GetGroupInput{ + GroupName: aws.String(rs.Primary.ID), + }) + if err != nil { + return err + } + + *res = *resp + + return nil + } +} + +func testAccCheckAWSGroupAttributes(group *iam.GetGroupOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + if *group.Group.GroupName != "test-group" { + return fmt.Errorf("Bad name: %s", *group.Group.GroupName) + } + + if *group.Group.Path != "/" { + return fmt.Errorf("Bad path: %s", *group.Group.Path) + } + + return nil + } +} + +const testAccAWSGroupConfig = ` +resource "aws_iam_group" "group" { + name = "test-group" + path = "/" +} +` diff --git a/builtin/providers/aws/resource_aws_iam_instance_profile.go b/builtin/providers/aws/resource_aws_iam_instance_profile.go new file mode 100644 index 000000000000..237d4348af39 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_instance_profile.go @@ -0,0 +1,188 @@ +package aws + +import ( + "fmt" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsIamInstanceProfile() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsIamInstanceProfileCreate, + Read: resourceAwsIamInstanceProfileRead, + Update: resourceAwsIamInstanceProfileUpdate, + Delete: resourceAwsIamInstanceProfileDelete, + + Schema: map[string]*schema.Schema{ + "arn": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "create_date": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "unique_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "path": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "/", + ForceNew: true, + }, + "roles": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + } +} + +func resourceAwsIamInstanceProfileCreate(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + name := d.Get("name").(string) + + request := &iam.CreateInstanceProfileInput{ + InstanceProfileName: aws.String(name), + Path: aws.String(d.Get("path").(string)), + } + + response, err := iamconn.CreateInstanceProfile(request) + if err == nil { + err = instanceProfileReadResult(d, response.InstanceProfile) + } + if err != nil { + return fmt.Errorf("Error creating IAM instance profile %s: %s", name, err) + } + + return instanceProfileSetRoles(d, iamconn) +} + +func instanceProfileAddRole(iamconn *iam.IAM, profileName, roleName string) error { + request := &iam.AddRoleToInstanceProfileInput{ + InstanceProfileName: aws.String(profileName), + RoleName: aws.String(roleName), + } + + _, err := iamconn.AddRoleToInstanceProfile(request) + return err +} + +func instanceProfileRemoveRole(iamconn *iam.IAM, profileName, roleName string) error { + request := &iam.RemoveRoleFromInstanceProfileInput{ + InstanceProfileName: aws.String(profileName), + RoleName: aws.String(roleName), + } + + _, err := iamconn.RemoveRoleFromInstanceProfile(request) + return err +} + +func instanceProfileSetRoles(d *schema.ResourceData, iamconn *iam.IAM) error { + oldInterface, newInterface := d.GetChange("roles") + oldRoles := oldInterface.(*schema.Set) + newRoles := newInterface.(*schema.Set) + + currentRoles := schema.CopySet(oldRoles) + + d.Partial(true) + + for _, role := range oldRoles.Difference(newRoles).List() { + err := instanceProfileRemoveRole(iamconn, d.Id(), role.(string)) + if err != nil { + return fmt.Errorf("Error removing role %s from IAM instance profile %s: %s", role, d.Id(), err) + } + currentRoles.Remove(role) + d.Set("roles", currentRoles) + d.SetPartial("roles") + } + + for _, role := range newRoles.Difference(oldRoles).List() { + err := instanceProfileAddRole(iamconn, d.Id(), role.(string)) + if err != nil { + return fmt.Errorf("Error adding role %s to IAM instance profile %s: %s", role, d.Id(), err) + } + currentRoles.Add(role) + d.Set("roles", currentRoles) + d.SetPartial("roles") + } + + d.Partial(false) + + return nil +} + +func resourceAwsIamInstanceProfileUpdate(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + if !d.HasChange("roles") { + return nil + } + + return instanceProfileSetRoles(d, iamconn) +} + +func resourceAwsIamInstanceProfileRead(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.GetInstanceProfileInput{ + InstanceProfileName: aws.String(d.Id()), + } + + result, err := iamconn.GetInstanceProfile(request) + if err != nil { + if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { + d.SetId("") + return nil + } + return fmt.Errorf("Error reading IAM instance profile %s: %s", d.Id(), err) + } + + return instanceProfileReadResult(d, result.InstanceProfile) +} + +func resourceAwsIamInstanceProfileDelete(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.DeleteInstanceProfileInput{ + InstanceProfileName: aws.String(d.Id()), + } + _, err := iamconn.DeleteInstanceProfile(request) + if err != nil { + return fmt.Errorf("Error deleting IAM instance profile %s: %s", d.Id(), err) + } + d.SetId("") + return nil +} + +func instanceProfileReadResult(d *schema.ResourceData, result *iam.InstanceProfile) error { + d.SetId(*result.InstanceProfileName) + if err := d.Set("name", result.InstanceProfileName); err != nil { + return err + } + if err := d.Set("path", result.Path); err != nil { + return err + } + + roles := &schema.Set{F: schema.HashString} + for _, role := range result.Roles { + roles.Add(*role.RoleName) + } + if err := d.Set("roles", roles); err != nil { + return err + } + + return nil +} diff --git a/builtin/providers/aws/resource_aws_iam_role.go b/builtin/providers/aws/resource_aws_iam_role.go new file mode 100644 index 000000000000..6a9bdc00ad47 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_role.go @@ -0,0 +1,112 @@ +package aws + +import ( + "fmt" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsIamRole() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsIamRoleCreate, + Read: resourceAwsIamRoleRead, + // TODO + //Update: resourceAwsIamRoleUpdate, + Delete: resourceAwsIamRoleDelete, + + Schema: map[string]*schema.Schema{ + "arn": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "unique_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "path": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "/", + ForceNew: true, + }, + "assume_role_policy": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsIamRoleCreate(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + name := d.Get("name").(string) + + request := &iam.CreateRoleInput{ + Path: aws.String(d.Get("path").(string)), + RoleName: aws.String(name), + AssumeRolePolicyDocument: aws.String(d.Get("assume_role_policy").(string)), + } + + createResp, err := iamconn.CreateRole(request) + if err != nil { + return fmt.Errorf("Error creating IAM Role %s: %s", name, err) + } + return resourceAwsIamRoleReadResult(d, createResp.Role) +} + +func resourceAwsIamRoleRead(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.GetRoleInput{ + RoleName: aws.String(d.Id()), + } + + getResp, err := iamconn.GetRole(request) + if err != nil { + if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX test me + d.SetId("") + return nil + } + return fmt.Errorf("Error reading IAM Role %s: %s", d.Id(), err) + } + return resourceAwsIamRoleReadResult(d, getResp.Role) +} + +func resourceAwsIamRoleReadResult(d *schema.ResourceData, role *iam.Role) error { + d.SetId(*role.RoleName) + if err := d.Set("name", role.RoleName); err != nil { + return err + } + if err := d.Set("arn", role.ARN); err != nil { + return err + } + if err := d.Set("path", role.Path); err != nil { + return err + } + if err := d.Set("unique_id", role.RoleID); err != nil { + return err + } + return nil +} + +func resourceAwsIamRoleDelete(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.DeleteRoleInput{ + RoleName: aws.String(d.Id()), + } + + if _, err := iamconn.DeleteRole(request); err != nil { + return fmt.Errorf("Error deleting IAM Role %s: %s", d.Id(), err) + } + return nil +} diff --git a/builtin/providers/aws/resource_aws_iam_role_policy.go b/builtin/providers/aws/resource_aws_iam_role_policy.go new file mode 100644 index 000000000000..0eb073a9d7d4 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_role_policy.go @@ -0,0 +1,110 @@ +package aws + +import ( + "fmt" + "net/url" + "strings" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsIamRolePolicy() *schema.Resource { + return &schema.Resource{ + // PutRolePolicy API is idempotent, so these can be the same. + Create: resourceAwsIamRolePolicyPut, + Update: resourceAwsIamRolePolicyPut, + + Read: resourceAwsIamRolePolicyRead, + Delete: resourceAwsIamRolePolicyDelete, + + Schema: map[string]*schema.Schema{ + "policy": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "role": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsIamRolePolicyPut(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.PutRolePolicyInput{ + RoleName: aws.String(d.Get("role").(string)), + PolicyName: aws.String(d.Get("name").(string)), + PolicyDocument: aws.String(d.Get("policy").(string)), + } + + if _, err := iamconn.PutRolePolicy(request); err != nil { + return fmt.Errorf("Error putting IAM role policy %s: %s", *request.PolicyName, err) + } + + d.SetId(fmt.Sprintf("%s:%s", *request.RoleName, *request.PolicyName)) + return nil +} + +func resourceAwsIamRolePolicyRead(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + role, name := resourceAwsIamRolePolicyParseId(d.Id()) + + request := &iam.GetRolePolicyInput{ + PolicyName: aws.String(name), + RoleName: aws.String(role), + } + + getResp, err := iamconn.GetRolePolicy(request) + if err != nil { + if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX test me + d.SetId("") + return nil + } + return fmt.Errorf("Error reading IAM policy %s from role %s: %s", name, role, err) + } + + if getResp.PolicyDocument == nil { + return fmt.Errorf("GetRolePolicy returned a nil policy document") + } + + policy, err := url.QueryUnescape(*getResp.PolicyDocument) + if err != nil { + return err + } + return d.Set("policy", policy) +} + +func resourceAwsIamRolePolicyDelete(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + role, name := resourceAwsIamRolePolicyParseId(d.Id()) + + request := &iam.DeleteRolePolicyInput{ + PolicyName: aws.String(name), + RoleName: aws.String(role), + } + + if _, err := iamconn.DeleteRolePolicy(request); err != nil { + return fmt.Errorf("Error deleting IAM role policy %s: %s", d.Id(), err) + } + return nil +} + +func resourceAwsIamRolePolicyParseId(id string) (roleName, policyName string) { + parts := strings.SplitN(id, ":", 2) + roleName = parts[0] + policyName = parts[1] + return +} diff --git a/builtin/providers/aws/resource_aws_iam_role_policy_test.go b/builtin/providers/aws/resource_aws_iam_role_policy_test.go new file mode 100644 index 000000000000..38f76dbfd9e3 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_role_policy_test.go @@ -0,0 +1,112 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSIAMRolePolicy(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckIAMRolePolicyDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccIAMRolePolicyConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMRolePolicy( + "aws_iam_role.role", + "aws_iam_role_policy.foo", + ), + ), + }, + resource.TestStep{ + Config: testAccIAMRolePolicyConfigUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMRolePolicy( + "aws_iam_role.role", + "aws_iam_role_policy.bar", + ), + ), + }, + }, + }) +} + +func testAccCheckIAMRolePolicyDestroy(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 +} + +func testAccCheckIAMRolePolicy( + iamRoleResource string, + iamRolePolicyResource string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[iamRoleResource] + if !ok { + return fmt.Errorf("Not Found: %s", iamRoleResource) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + policy, ok := s.RootModule().Resources[iamRolePolicyResource] + if !ok { + return fmt.Errorf("Not Found: %s", iamRolePolicyResource) + } + + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + role, name := resourceAwsIamRolePolicyParseId(policy.Primary.ID) + _, err := iamconn.GetRolePolicy(&iam.GetRolePolicyInput{ + RoleName: aws.String(role), + PolicyName: aws.String(name), + }) + + if err != nil { + return err + } + + return nil + } +} + +const testAccIAMRolePolicyConfig = ` +resource "aws_iam_role" "role" { + name = "test_role" + path = "/" +} + +resource "aws_iam_role_policy" "foo" { + name = "foo_policy" + role = "${aws_iam_role.role.name}" + policy = "{\"Version\":\"2012-10-17\",\"Statement\":{\"Effect\":\"Allow\",\"Action\":\"*\",\"Resource\":\"*\"}}" +} +` + +const testAccIAMRolePolicyConfigUpdate = ` +resource "aws_iam_role" "role" { + name = "test_role" + path = "/" +} + +resource "aws_iam_role_policy" "foo" { + name = "foo_policy" + role = "${aws_iam_role.role.name}" + policy = "{\"Version\":\"2012-10-17\",\"Statement\":{\"Effect\":\"Allow\",\"Action\":\"*\",\"Resource\":\"*\"}}" +} + +resource "aws_iam_role_policy" "bar" { + name = "bar_policy" + role = "${aws_iam_role.role.name}" + policy = "{\"Version\":\"2012-10-17\",\"Statement\":{\"Effect\":\"Allow\",\"Action\":\"*\",\"Resource\":\"*\"}}" +} +` diff --git a/builtin/providers/aws/resource_aws_iam_role_test.go b/builtin/providers/aws/resource_aws_iam_role_test.go new file mode 100644 index 000000000000..0a42a3c3229a --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_role_test.go @@ -0,0 +1,110 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSRole_normal(t *testing.T) { + var conf iam.GetRoleOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRoleDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSRoleConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists("aws_iam_role.role", &conf), + testAccCheckAWSRoleAttributes(&conf), + ), + }, + }, + }) +} + +func testAccCheckAWSRoleDestroy(s *terraform.State) error { + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_iam_role" { + continue + } + + // Try to get role + _, err := iamconn.GetRole(&iam.GetRoleInput{ + RoleName: aws.String(rs.Primary.ID), + }) + if err == nil { + return fmt.Errorf("still exist.") + } + + // Verify the error is what we want + ec2err, ok := err.(aws.APIError) + if !ok { + return err + } + if ec2err.Code != "NoSuchEntity" { + return err + } + } + + return nil +} + +func testAccCheckAWSRoleExists(n string, res *iam.GetRoleOutput) 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 Role name is set") + } + + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + + resp, err := iamconn.GetRole(&iam.GetRoleInput{ + RoleName: aws.String(rs.Primary.ID), + }) + if err != nil { + return err + } + + *res = *resp + + return nil + } +} + +func testAccCheckAWSRoleAttributes(role *iam.GetRoleOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + if *role.Role.RoleName != "test-role" { + return fmt.Errorf("Bad name: %s", *role.Role.RoleName) + } + + if *role.Role.Path != "/" { + return fmt.Errorf("Bad path: %s", *role.Role.Path) + } + + if *role.Role.AssumeRolePolicyDocument != "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ec2.amazonaws.com\"]},\"Action\":[\"sts:AssumeRole\"]}]}" { + return fmt.Errorf("Bad policy: %s", *role.Role.AssumeRolePolicyDocument) + } + return nil + } +} + +const testAccAWSRoleConfig = ` +resource "aws_iam_role" "role" { + name = "test-role" + path = "/" + assume_role_policy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ec2.amazonaws.com\"]},\"Action\":[\"sts:AssumeRole\"]}]}" +} +` diff --git a/builtin/providers/aws/resource_aws_iam_user.go b/builtin/providers/aws/resource_aws_iam_user.go new file mode 100644 index 000000000000..73c5ac8f0505 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_user.go @@ -0,0 +1,115 @@ +package aws + +import ( + "fmt" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsIamUser() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsIamUserCreate, + Read: resourceAwsIamUserRead, + // There is an UpdateUser API call, but goamz doesn't support it yet. + // XXX but we aren't using goamz anymore. + //Update: resourceAwsIamUserUpdate, + Delete: resourceAwsIamUserDelete, + + Schema: map[string]*schema.Schema{ + "arn": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + /* + The UniqueID could be used as the Id(), but none of the API + calls allow specifying a user by the UniqueID: they require the + name. The only way to locate a user by UniqueID is to list them + all and that would make this provider unnecessarilly complex + and inefficient. Still, there are other reasons one might want + the UniqueID, so we can make it availible. + */ + "unique_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "path": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "/", + ForceNew: true, + }, + }, + } +} + +func resourceAwsIamUserCreate(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + name := d.Get("name").(string) + + request := &iam.CreateUserInput{ + Path: aws.String(d.Get("path").(string)), + UserName: aws.String(name), + } + + createResp, err := iamconn.CreateUser(request) + if err != nil { + return fmt.Errorf("Error creating IAM User %s: %s", name, err) + } + return resourceAwsIamUserReadResult(d, createResp.User) +} + +func resourceAwsIamUserRead(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.GetUserInput{ + UserName: aws.String(d.Id()), + } + + getResp, err := iamconn.GetUser(request) + if err != nil { + if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX test me + d.SetId("") + return nil + } + return fmt.Errorf("Error reading IAM User %s: %s", d.Id(), err) + } + return resourceAwsIamUserReadResult(d, getResp.User) +} + +func resourceAwsIamUserReadResult(d *schema.ResourceData, user *iam.User) error { + d.SetId(*user.UserName) + if err := d.Set("name", user.UserName); err != nil { + return err + } + if err := d.Set("arn", user.ARN); err != nil { + return err + } + if err := d.Set("path", user.Path); err != nil { + return err + } + if err := d.Set("unique_id", user.UserID); err != nil { + return err + } + return nil +} + +func resourceAwsIamUserDelete(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.DeleteUserInput{ + UserName: aws.String(d.Id()), + } + + if _, err := iamconn.DeleteUser(request); err != nil { + return fmt.Errorf("Error deleting IAM User %s: %s", d.Id(), err) + } + return nil +} diff --git a/builtin/providers/aws/resource_aws_iam_user_policy.go b/builtin/providers/aws/resource_aws_iam_user_policy.go new file mode 100644 index 000000000000..96ea6afc0e88 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_user_policy.go @@ -0,0 +1,110 @@ +package aws + +import ( + "fmt" + "net/url" + "strings" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsIamUserPolicy() *schema.Resource { + return &schema.Resource{ + // PutUserPolicy API is idempotent, so these can be the same. + Create: resourceAwsIamUserPolicyPut, + Update: resourceAwsIamUserPolicyPut, + + Read: resourceAwsIamUserPolicyRead, + Delete: resourceAwsIamUserPolicyDelete, + + Schema: map[string]*schema.Schema{ + "policy": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "user": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsIamUserPolicyPut(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.PutUserPolicyInput{ + UserName: aws.String(d.Get("user").(string)), + PolicyName: aws.String(d.Get("name").(string)), + PolicyDocument: aws.String(d.Get("policy").(string)), + } + + if _, err := iamconn.PutUserPolicy(request); err != nil { + return fmt.Errorf("Error putting IAM user policy %s: %s", *request.PolicyName, err) + } + + d.SetId(fmt.Sprintf("%s:%s", *request.UserName, *request.PolicyName)) + return nil +} + +func resourceAwsIamUserPolicyRead(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + user, name := resourceAwsIamUserPolicyParseId(d.Id()) + + request := &iam.GetUserPolicyInput{ + PolicyName: aws.String(name), + UserName: aws.String(user), + } + + getResp, err := iamconn.GetUserPolicy(request) + if err != nil { + if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX test me + d.SetId("") + return nil + } + return fmt.Errorf("Error reading IAM policy %s from user %s: %s", name, user, err) + } + + if getResp.PolicyDocument == nil { + return fmt.Errorf("GetUserPolicy returned a nil policy document") + } + + policy, err := url.QueryUnescape(*getResp.PolicyDocument) + if err != nil { + return err + } + return d.Set("policy", policy) +} + +func resourceAwsIamUserPolicyDelete(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + user, name := resourceAwsIamUserPolicyParseId(d.Id()) + + request := &iam.DeleteUserPolicyInput{ + PolicyName: aws.String(name), + UserName: aws.String(user), + } + + if _, err := iamconn.DeleteUserPolicy(request); err != nil { + return fmt.Errorf("Error deleting IAM user policy %s: %s", d.Id(), err) + } + return nil +} + +func resourceAwsIamUserPolicyParseId(id string) (userName, policyName string) { + parts := strings.SplitN(id, ":", 2) + userName = parts[0] + policyName = parts[1] + return +} diff --git a/builtin/providers/aws/resource_aws_iam_user_policy_test.go b/builtin/providers/aws/resource_aws_iam_user_policy_test.go new file mode 100644 index 000000000000..2a2147e567de --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_user_policy_test.go @@ -0,0 +1,112 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSIAMUserPolicy(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckIAMUserPolicyDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccIAMUserPolicyConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMUserPolicy( + "aws_iam_user.user", + "aws_iam_user_policy.foo", + ), + ), + }, + resource.TestStep{ + Config: testAccIAMUserPolicyConfigUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMUserPolicy( + "aws_iam_user.user", + "aws_iam_user_policy.bar", + ), + ), + }, + }, + }) +} + +func testAccCheckIAMUserPolicyDestroy(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 +} + +func testAccCheckIAMUserPolicy( + iamUserResource string, + iamUserPolicyResource string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[iamUserResource] + if !ok { + return fmt.Errorf("Not Found: %s", iamUserResource) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + policy, ok := s.RootModule().Resources[iamUserPolicyResource] + if !ok { + return fmt.Errorf("Not Found: %s", iamUserPolicyResource) + } + + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + username, name := resourceAwsIamUserPolicyParseId(policy.Primary.ID) + _, err := iamconn.GetUserPolicy(&iam.GetUserPolicyInput{ + UserName: aws.String(username), + PolicyName: aws.String(name), + }) + + if err != nil { + return err + } + + return nil + } +} + +const testAccIAMUserPolicyConfig = ` +resource "aws_iam_user" "user" { + name = "test_user" + path = "/" +} + +resource "aws_iam_user_policy" "foo" { + name = "foo_policy" + user = "${aws_iam_user.user.name}" + policy = "{\"Version\":\"2012-10-17\",\"Statement\":{\"Effect\":\"Allow\",\"Action\":\"*\",\"Resource\":\"*\"}}" +} +` + +const testAccIAMUserPolicyConfigUpdate = ` +resource "aws_iam_user" "user" { + name = "test_user" + path = "/" +} + +resource "aws_iam_user_policy" "foo" { + name = "foo_policy" + user = "${aws_iam_user.user.name}" + policy = "{\"Version\":\"2012-10-17\",\"Statement\":{\"Effect\":\"Allow\",\"Action\":\"*\",\"Resource\":\"*\"}}" +} + +resource "aws_iam_user_policy" "bar" { + name = "bar_policy" + user = "${aws_iam_user.user.name}" + policy = "{\"Version\":\"2012-10-17\",\"Statement\":{\"Effect\":\"Allow\",\"Action\":\"*\",\"Resource\":\"*\"}}" +} +` diff --git a/builtin/providers/aws/resource_aws_iam_user_test.go b/builtin/providers/aws/resource_aws_iam_user_test.go new file mode 100644 index 000000000000..513636f98613 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_user_test.go @@ -0,0 +1,106 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/iam" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSUser_normal(t *testing.T) { + var conf iam.GetUserOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSUserDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSUserConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSUserExists("aws_iam_user.user", &conf), + testAccCheckAWSUserAttributes(&conf), + ), + }, + }, + }) +} + +func testAccCheckAWSUserDestroy(s *terraform.State) error { + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_iam_user" { + continue + } + + // Try to get user + _, err := iamconn.GetUser(&iam.GetUserInput{ + UserName: aws.String(rs.Primary.ID), + }) + if err == nil { + return fmt.Errorf("still exist.") + } + + // Verify the error is what we want + ec2err, ok := err.(aws.APIError) + if !ok { + return err + } + if ec2err.Code != "NoSuchEntity" { + return err + } + } + + return nil +} + +func testAccCheckAWSUserExists(n string, res *iam.GetUserOutput) 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 User name is set") + } + + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + + resp, err := iamconn.GetUser(&iam.GetUserInput{ + UserName: aws.String(rs.Primary.ID), + }) + if err != nil { + return err + } + + *res = *resp + + return nil + } +} + +func testAccCheckAWSUserAttributes(user *iam.GetUserOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + if *user.User.UserName != "test-user" { + return fmt.Errorf("Bad name: %s", *user.User.UserName) + } + + if *user.User.Path != "/" { + return fmt.Errorf("Bad path: %s", *user.User.Path) + } + + return nil + } +} + +const testAccAWSUserConfig = ` +resource "aws_iam_user" "user" { + name = "test-user" + path = "/" +} +` diff --git a/builtin/providers/aws/resource_aws_instance.go b/builtin/providers/aws/resource_aws_instance.go index 3874af81a2a1..c523e8630d5a 100644 --- a/builtin/providers/aws/resource_aws_instance.go +++ b/builtin/providers/aws/resource_aws_instance.go @@ -100,9 +100,7 @@ func resourceAwsInstance() *schema.Resource { Computed: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, "vpc_security_group_ids": &schema.Schema{ diff --git a/builtin/providers/aws/resource_aws_launch_configuration.go b/builtin/providers/aws/resource_aws_launch_configuration.go index 3ed551c86798..1537d1f0d1c2 100644 --- a/builtin/providers/aws/resource_aws_launch_configuration.go +++ b/builtin/providers/aws/resource_aws_launch_configuration.go @@ -76,9 +76,7 @@ func resourceAwsLaunchConfiguration() *schema.Resource { Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, "associate_public_ip_address": &schema.Schema{ diff --git a/builtin/providers/aws/resource_aws_network_interface.go b/builtin/providers/aws/resource_aws_network_interface.go index 00c913b119d0..095bfb955b2b 100644 --- a/builtin/providers/aws/resource_aws_network_interface.go +++ b/builtin/providers/aws/resource_aws_network_interface.go @@ -34,9 +34,7 @@ func resourceAwsNetworkInterface() *schema.Resource { Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, "security_groups": &schema.Schema{ @@ -44,9 +42,7 @@ func resourceAwsNetworkInterface() *schema.Resource { Optional: true, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, "attachment": &schema.Schema{ diff --git a/builtin/providers/aws/resource_aws_route53_record.go b/builtin/providers/aws/resource_aws_route53_record.go index a5b47d0b17ca..d3b030ec4b80 100644 --- a/builtin/providers/aws/resource_aws_route53_record.go +++ b/builtin/providers/aws/resource_aws_route53_record.go @@ -6,7 +6,6 @@ import ( "strings" "time" - "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" @@ -49,9 +48,7 @@ func resourceAwsRoute53Record() *schema.Resource { Type: schema.TypeSet, Elem: &schema.Schema{Type: schema.TypeString}, Required: true, - Set: func(v interface{}) int { - return hashcode.String(v.(string)) - }, + Set: schema.HashString, }, }, } diff --git a/builtin/providers/aws/structure_test.go b/builtin/providers/aws/structure_test.go index 9c7eaf88d411..6ec80274cedc 100644 --- a/builtin/providers/aws/structure_test.go +++ b/builtin/providers/aws/structure_test.go @@ -10,7 +10,6 @@ import ( "github.com/awslabs/aws-sdk-go/service/rds" "github.com/awslabs/aws-sdk-go/service/route53" "github.com/hashicorp/terraform/flatmap" - "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" ) @@ -38,9 +37,7 @@ func testConf() map[string]string { } func TestexpandIPPerms(t *testing.T) { - hash := func(v interface{}) int { - return hashcode.String(v.(string)) - } + hash := schema.HashString expanded := []interface{}{ map[string]interface{}{ @@ -121,9 +118,7 @@ func TestexpandIPPerms(t *testing.T) { } func TestExpandIPPerms_nonVPC(t *testing.T) { - hash := func(v interface{}) int { - return hashcode.String(v.(string)) - } + hash := schema.HashString expanded := []interface{}{ map[string]interface{}{ diff --git a/helper/schema/set.go b/helper/schema/set.go index 965ad6f8b8a5..6496a5d769c6 100644 --- a/helper/schema/set.go +++ b/helper/schema/set.go @@ -5,8 +5,16 @@ import ( "reflect" "sort" "sync" + + "github.com/hashicorp/terraform/helper/hashcode" ) +// HashString hashes strings. If you want a Set of strings, this is the +// SchemaSetFunc you want. +func HashString(v interface{}) int { + return hashcode.String(v.(string)) +} + // Set is a set data structure that is returned for elements of type // TypeSet. type Set struct { @@ -27,11 +35,21 @@ func NewSet(f SchemaSetFunc, items []interface{}) *Set { return s } +// CopySet returns a copy of another set. +func CopySet(otherSet *Set) *Set { + return NewSet(otherSet.F, otherSet.List()) +} + // Add adds an item to the set if it isn't already in the set. func (s *Set) Add(item interface{}) { s.add(item) } +// Remove removes an item if it's already in the set. Idempotent. +func (s *Set) Remove(item interface{}) { + s.remove(item) +} + // Contains checks if the set has the given item. func (s *Set) Contains(item interface{}) bool { _, ok := s.m[s.F(item)] @@ -130,6 +148,15 @@ func (s *Set) add(item interface{}) int { return code } +func (s *Set) remove(item interface{}) int { + s.once.Do(s.init) + + code := s.F(item) + delete(s.m, code) + + return code +} + func (s *Set) index(item interface{}) int { return sort.SearchInts(s.listCode(), s.F(item)) } diff --git a/website/source/docs/providers/aws/r/iam_access_key.html.markdown b/website/source/docs/providers/aws/r/iam_access_key.html.markdown new file mode 100644 index 000000000000..042372c3cb94 --- /dev/null +++ b/website/source/docs/providers/aws/r/iam_access_key.html.markdown @@ -0,0 +1,59 @@ +--- +layout: "aws" +page_title: "AWS: aws_iam_access_key" +sidebar_current: "docs-aws-resource-iam-access-key" +description: |- + Provides an IAM access key. This is a set of credentials that allow API requests to be made as an IAM user. +--- + +# aws\_iam\_access\_key + +Provides an IAM access key. This is a set of credentials that allow API requests to be made as an IAM user. + +## Example Usage + +``` +resource "aws_iam_user" "lb" { + name = "loadbalancer" + path = "/system/" +} + +resource "aws_iam_access_key" "lb" { + user = "${aws_iam_user.lb.name}" + status = "Active" +} + +resource "aws_iam_user_policy" "lb_ro" { + name = "test" + user = "${aws_iam_user.lb.name}" + policy = <aws_elb + > + aws_iam_access_key + + + > + aws_iam_user + + + > + aws_iam_user_policy + + > aws_instance