From b082117e92664d7173728fc7995c8cee9f7d4bbb Mon Sep 17 00:00:00 2001 From: Phil Frost Date: Fri, 6 Feb 2015 10:34:24 -0500 Subject: [PATCH] Implement AWS IAM resources - Users - Groups - Roles - Inline policies for the above three - Instance profiles - Managed policies - Access keys This is most of the data types provided by IAM. There are a few things missing, but the functionality here is probably sufficient for 95% of the cases. Makes a dent in #28. --- builtin/providers/aws/provider.go | 23 +- .../aws/resource_aws_iam_access_key.go | 116 +++++++++ .../providers/aws/resource_aws_iam_group.go | 106 ++++++++ .../aws/resource_aws_iam_group_policy.go | 110 +++++++++ .../aws/resource_aws_iam_instance_profile.go | 205 ++++++++++++++++ .../resource_aws_iam_instance_profile_test.go | 31 +++ .../providers/aws/resource_aws_iam_policy.go | 226 ++++++++++++++++++ .../providers/aws/resource_aws_iam_role.go | 112 +++++++++ .../aws/resource_aws_iam_role_policy.go | 110 +++++++++ .../providers/aws/resource_aws_iam_user.go | 115 +++++++++ .../aws/resource_aws_iam_user_policy.go | 110 +++++++++ helper/schema/set.go | 19 ++ .../aws/r/iam_access_key.html.markdown | 59 +++++ .../providers/aws/r/iam_user.html.markdown | 60 +++++ .../aws/r/iam_user_policy.html.markdown | 57 +++++ website/source/layouts/aws.erb | 12 + 16 files changed, 1464 insertions(+), 7 deletions(-) create mode 100644 builtin/providers/aws/resource_aws_iam_access_key.go create mode 100644 builtin/providers/aws/resource_aws_iam_group.go create mode 100644 builtin/providers/aws/resource_aws_iam_group_policy.go create mode 100644 builtin/providers/aws/resource_aws_iam_instance_profile.go create mode 100644 builtin/providers/aws/resource_aws_iam_instance_profile_test.go create mode 100644 builtin/providers/aws/resource_aws_iam_policy.go create mode 100644 builtin/providers/aws/resource_aws_iam_role.go create mode 100644 builtin/providers/aws/resource_aws_iam_role_policy.go create mode 100644 builtin/providers/aws/resource_aws_iam_user.go create mode 100644 builtin/providers/aws/resource_aws_iam_user_policy.go create mode 100644 website/source/docs/providers/aws/r/iam_access_key.html.markdown create mode 100644 website/source/docs/providers/aws/r/iam_user.html.markdown create mode 100644 website/source/docs/providers/aws/r/iam_user_policy.html.markdown diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index e8e6d12bd97b..ee40ae6890f8 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -83,19 +83,28 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "aws_autoscaling_group": resourceAwsAutoscalingGroup(), "aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(), + "aws_autoscaling_group": resourceAwsAutoscalingGroup(), "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_eip": resourceAwsEip(), "aws_elasticache_cluster": resourceAwsElasticacheCluster(), - "aws_elasticache_subnet_group": resourceAwsElasticacheSubnetGroup(), "aws_elasticache_security_group": resourceAwsElasticacheSecurityGroup(), - "aws_eip": resourceAwsEip(), + "aws_elasticache_subnet_group": resourceAwsElasticacheSubnetGroup(), "aws_elb": resourceAwsElb(), + "aws_iam_access_key": resourceAwsIamAccessKey(), + "aws_iam_group_policy": resourceAwsIamGroupPolicy(), + "aws_iam_group": resourceAwsIamGroup(), + "aws_iam_instance_profile": resourceAwsIamInstanceProfile(), + "aws_iam_policy": resourceAwsIamPolicy(), + "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(), @@ -107,15 +116,15 @@ func Provider() terraform.ResourceProvider { "aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(), "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(), - "aws_vpc": resourceAwsVpc(), - "aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(), - "aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(), "aws_vpc_dhcp_options_association": resourceAwsVpcDhcpOptionsAssociation(), + "aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(), + "aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(), + "aws_vpc": resourceAwsVpc(), "aws_vpn_connection": resourceAwsVpnConnection(), "aws_vpn_connection_route": resourceAwsVpnConnectionRoute(), "aws_vpn_gateway": resourceAwsVpnGateway(), 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_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..1e6c244a5d26 --- /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) + + 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) + + 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(d *schema.ResourceData) (groupName, policyName string) { + parts := strings.SplitN(d.Id(), ":", 2) + groupName = parts[0] + policyName = parts[1] + return +} 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..506935d8eb80 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_instance_profile.go @@ -0,0 +1,205 @@ +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) + if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { + return nil + } + 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 instanceProfileRemoveAllRoles(d *schema.ResourceData, iamconn *iam.IAM) error { + for _, role := range d.Get("roles").(*schema.Set).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) + } + } + 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 + + if err := instanceProfileRemoveAllRoles(d, iamconn); err != nil { + return err + } + + 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_instance_profile_test.go b/builtin/providers/aws/resource_aws_iam_instance_profile_test.go new file mode 100644 index 000000000000..0d50024ecca6 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_instance_profile_test.go @@ -0,0 +1,31 @@ +package aws + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccAwsIamInstanceProfile(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAwsIamInstanceProfileConfig, + }, + }, + }) +} + +const testAccAwsIamInstanceProfileConfig = ` +resource "aws_iam_role" "test" { + name = "test" + assume_role_policy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ec2.amazonaws.com\"]},\"Action\":[\"sts:AssumeRole\"]}]}" +} + +resource "aws_iam_instance_profile" "test" { + name = "test" + roles = ["${aws_iam_role.test.name}"] +} +` diff --git a/builtin/providers/aws/resource_aws_iam_policy.go b/builtin/providers/aws/resource_aws_iam_policy.go new file mode 100644 index 000000000000..9d4e3d6dd159 --- /dev/null +++ b/builtin/providers/aws/resource_aws_iam_policy.go @@ -0,0 +1,226 @@ +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 resourceAwsIamPolicy() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsIamPolicyCreate, + Read: resourceAwsIamPolicyRead, + Update: resourceAwsIamPolicyUpdate, + Delete: resourceAwsIamPolicyDelete, + + Schema: map[string]*schema.Schema{ + "description": &schema.Schema{ + Type: schema.TypeString, + ForceNew: true, + Optional: true, + }, + "path": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "/", + ForceNew: true, + }, + "policy": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "arn": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsIamPolicyCreate(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + name := d.Get("name").(string) + + request := &iam.CreatePolicyInput{ + Description: aws.String(d.Get("description").(string)), + Path: aws.String(d.Get("path").(string)), + PolicyDocument: aws.String(d.Get("policy").(string)), + PolicyName: aws.String(name), + } + + response, err := iamconn.CreatePolicy(request) + if err != nil { + return fmt.Errorf("Error creating IAM policy %s: %#v", name, err) + } + + return readIamPolicy(d, response.Policy) +} + +func resourceAwsIamPolicyRead(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + request := &iam.GetPolicyInput{ + PolicyARN: aws.String(d.Id()), + } + + response, err := iamconn.GetPolicy(request) + if err != nil { + if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { + d.SetId("") + return nil + } + return fmt.Errorf("Error reading IAM policy %s: %#v", d.Id(), err) + } + + return readIamPolicy(d, response.Policy) +} + +func resourceAwsIamPolicyUpdate(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + if err := iamPolicyPruneVersions(d.Id(), iamconn); err != nil { + return err + } + + if !d.HasChange("policy") { + return nil + } + request := &iam.CreatePolicyVersionInput{ + PolicyARN: aws.String(d.Id()), + PolicyDocument: aws.String(d.Get("policy").(string)), + SetAsDefault: aws.Boolean(true), + } + + if _, err := iamconn.CreatePolicyVersion(request); err != nil { + return fmt.Errorf("Error updating IAM policy %s: %#v", d.Id(), err) + } + return nil +} + +func resourceAwsIamPolicyDelete(d *schema.ResourceData, meta interface{}) error { + iamconn := meta.(*AWSClient).iamconn + + if err := iamPolicyDeleteNondefaultVersions(d.Id(), iamconn); err != nil { + return err + } + + request := &iam.DeletePolicyInput{ + PolicyARN: aws.String(d.Id()), + } + + _, err := iamconn.DeletePolicy(request) + if err != nil { + if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { + return nil + } + return fmt.Errorf("Error reading IAM policy %s: %#v", d.Id(), err) + } + return nil +} + +// iamPolicyPruneVersions deletes the oldest versions. +// +// Old versions are deleted until there are 4 or less remaining, which means at +// least one more can be created before hitting the maximum of 5. +// +// The default version is never deleted. + +func iamPolicyPruneVersions(arn string, iamconn *iam.IAM) error { + versions, err := iamPolicyListVersions(arn, iamconn) + if err != nil { + return err + } + if len(versions) < 5 { + return nil + } + + var oldestVersion *iam.PolicyVersion + + for _, version := range versions { + if *version.IsDefaultVersion { + continue + } + if oldestVersion == nil || + version.CreateDate.Before(*oldestVersion.CreateDate) { + oldestVersion = version + } + } + + if err := iamPolicyDeleteVersion(arn, *oldestVersion.VersionID, iamconn); err != nil { + return err + } + return nil +} + +func iamPolicyDeleteNondefaultVersions(arn string, iamconn *iam.IAM) error { + versions, err := iamPolicyListVersions(arn, iamconn) + if err != nil { + return err + } + + for _, version := range versions { + if *version.IsDefaultVersion { + continue + } + if err := iamPolicyDeleteVersion(arn, *version.VersionID, iamconn); err != nil { + return err + } + } + + return nil +} + +func iamPolicyDeleteVersion(arn, versionID string, iamconn *iam.IAM) error { + request := &iam.DeletePolicyVersionInput{ + PolicyARN: aws.String(arn), + VersionID: aws.String(versionID), + } + + _, err := iamconn.DeletePolicyVersion(request) + if err != nil { + return fmt.Errorf("Error deleting version %s from IAM policy %s: %#v", versionID, arn, err) + } + return nil +} + +func iamPolicyListVersions(arn string, iamconn *iam.IAM) ([]*iam.PolicyVersion, error) { + request := &iam.ListPolicyVersionsInput{ + PolicyARN: aws.String(arn), + } + + response, err := iamconn.ListPolicyVersions(request) + if err != nil { + return nil, fmt.Errorf("Error listing versions for IAM policy %s: %#v", arn, err) + } + return response.Versions, nil +} + +func readIamPolicy(d *schema.ResourceData, policy *iam.Policy) error { + d.SetId(*policy.ARN) + if policy.Description != nil { + // the description isn't present in the response to CreatePolicy. + if err := d.Set("description", *policy.Description); err != nil { + return err + } + } + if err := d.Set("path", *policy.Path); err != nil { + return err + } + if err := d.Set("name", *policy.PolicyName); err != nil { + return err + } + if err := d.Set("arn", *policy.ARN); err != nil { + return err + } + // TODO: set policy + + 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..854803f24f3e --- /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) + + 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) + + 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(d *schema.ResourceData) (userName, policyName string) { + parts := strings.SplitN(d.Id(), ":", 2) + userName = parts[0] + policyName = parts[1] + return +} 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..3fb97b29dc6f --- /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) + + 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) + + 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(d *schema.ResourceData) (userName, policyName string) { + parts := strings.SplitN(d.Id(), ":", 2) + userName = parts[0] + policyName = parts[1] + return +} diff --git a/helper/schema/set.go b/helper/schema/set.go index 5a94369664b0..7176b86308ad 100644 --- a/helper/schema/set.go +++ b/helper/schema/set.go @@ -35,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.hash(item)] @@ -147,6 +157,15 @@ func (s *Set) hash(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.hash(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