From 38687560f8c7ce361ce86ed3033ea1f2e65c9fe3 Mon Sep 17 00:00:00 2001 From: Matt Burgess <549318+mattburgess@users.noreply.github.com> Date: Sun, 17 Jul 2022 13:11:34 +0100 Subject: [PATCH 1/2] Add RolesAnywhere Profile resource --- .changelog/25850.txt | 3 + internal/provider/provider.go | 1 + internal/service/rolesanywhere/find.go | 26 ++ internal/service/rolesanywhere/flex.go | 13 + internal/service/rolesanywhere/profile.go | 265 ++++++++++++++++++ .../service/rolesanywhere/profile_test.go | 227 +++++++++++++++ .../r/rolesanywhere_profile.html.markdown | 69 +++++ 7 files changed, 604 insertions(+) create mode 100644 .changelog/25850.txt create mode 100644 internal/service/rolesanywhere/flex.go create mode 100644 internal/service/rolesanywhere/profile.go create mode 100644 internal/service/rolesanywhere/profile_test.go create mode 100644 website/docs/r/rolesanywhere_profile.html.markdown diff --git a/.changelog/25850.txt b/.changelog/25850.txt new file mode 100644 index 000000000000..a2d1efe4fb80 --- /dev/null +++ b/.changelog/25850.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_rolesanywhere_profile +``` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 6137ae853a7a..d3da7540a8ff 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1825,6 +1825,7 @@ func Provider() *schema.Provider { "aws_resourcegroups_group": resourcegroups.ResourceGroup(), + "aws_rolesanywhere_profile": rolesanywhere.ResourceProfile(), "aws_rolesanywhere_trust_anchor": rolesanywhere.ResourceTrustAnchor(), "aws_route53_delegation_set": route53.ResourceDelegationSet(), diff --git a/internal/service/rolesanywhere/find.go b/internal/service/rolesanywhere/find.go index 4144e21394b2..13311b48bc3a 100644 --- a/internal/service/rolesanywhere/find.go +++ b/internal/service/rolesanywhere/find.go @@ -11,6 +11,32 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) +func FindProfileByID(ctx context.Context, conn *rolesanywhere.Client, id string) (*types.ProfileDetail, error) { + in := &rolesanywhere.GetProfileInput{ + ProfileId: aws.String(id), + } + + out, err := conn.GetProfile(ctx, in) + + var resourceNotFoundException *types.ResourceNotFoundException + if errors.As(err, &resourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + if err != nil { + return nil, err + } + + if out == nil || out.Profile == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out.Profile, nil +} + func FindTrustAnchorByID(ctx context.Context, conn *rolesanywhere.Client, id string) (*types.TrustAnchorDetail, error) { in := &rolesanywhere.GetTrustAnchorInput{ TrustAnchorId: aws.String(id), diff --git a/internal/service/rolesanywhere/flex.go b/internal/service/rolesanywhere/flex.go new file mode 100644 index 000000000000..88cd69bc6a6b --- /dev/null +++ b/internal/service/rolesanywhere/flex.go @@ -0,0 +1,13 @@ +package rolesanywhere + +func expandStringList(tfList []interface{}) []string { + var result []string + + for _, rawVal := range tfList { + if v, ok := rawVal.(string); ok && v != "" { + result = append(result, v) + } + } + + return result +} diff --git a/internal/service/rolesanywhere/profile.go b/internal/service/rolesanywhere/profile.go new file mode 100644 index 000000000000..ebc5502b31f4 --- /dev/null +++ b/internal/service/rolesanywhere/profile.go @@ -0,0 +1,265 @@ +package rolesanywhere + +import ( + "context" + "errors" + "log" + + "github.com/aws/aws-sdk-go-v2/service/rolesanywhere" + "github.com/aws/aws-sdk-go-v2/service/rolesanywhere/types" + "github.com/aws/aws-sdk-go/aws" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +func ResourceProfile() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceProfileCreate, + ReadContext: resourceProfileRead, + UpdateContext: resourceProfileUpdate, + DeleteContext: resourceProfileDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "duration_seconds": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + }, + "managed_policy_arns": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "require_instance_properties": { + Type: schema.TypeBool, + Optional: true, + }, + "role_arns": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "session_policy": { + Type: schema.TypeString, + Optional: true, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + }, + CustomizeDiff: verify.SetTagsDiff, + } +} + +func resourceProfileCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).RolesAnywhereConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) + name := d.Get("name").(string) + + input := &rolesanywhere.CreateProfileInput{ + Name: aws.String(name), + RoleArns: expandStringList(d.Get("role_arns").(*schema.Set).List()), + Tags: Tags(tags.IgnoreAWS()), + } + + if v, ok := d.GetOk("duration_seconds"); ok { + input.DurationSeconds = aws.Int32(int32(v.(int))) + } + + if v, ok := d.GetOk("enabled"); ok { + input.Enabled = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("managed_policy_arns"); ok { + input.ManagedPolicyArns = expandStringList(v.(*schema.Set).List()) + } + + if v, ok := d.GetOk("require_instance_properties"); ok { + input.RequireInstanceProperties = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("session_policy"); ok { + input.SessionPolicy = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Creating RolesAnywhere Profile (%s): %#v", d.Id(), input) + output, err := conn.CreateProfile(ctx, input) + + if err != nil { + return diag.Errorf("creating RolesAnywhere Profile (%s): %s", name, err) + } + + d.SetId(aws.StringValue(output.Profile.ProfileId)) + + return resourceProfileRead(ctx, d, meta) +} + +func resourceProfileRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).RolesAnywhereConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + + profile, err := FindProfileByID(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] RolesAnywhere Profile (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("reading RolesAnywhere Profile (%s): %s", d.Id(), err) + } + + d.Set("arn", profile.ProfileArn) + d.Set("duration_seconds", profile.DurationSeconds) + d.Set("enabled", profile.Enabled) + d.Set("managed_policy_arns", profile.ManagedPolicyArns) + d.Set("name", profile.Name) + d.Set("require_instance_properties", profile.RequireInstanceProperties) + d.Set("role_arns", profile.RoleArns) + d.Set("session_policy", profile.SessionPolicy) + + tags, err := ListTags(ctx, conn, d.Get("arn").(string)) + if err != nil { + return diag.Errorf("listing tags for RolesAnywhere Profile (%s): %s", d.Id(), err) + } + + tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.Errorf("setting tags: %s", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return diag.Errorf("setting tags_all: %s", err) + } + + return nil +} + +func resourceProfileUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).RolesAnywhereConn + + if d.HasChangesExcept("enabled", "tags_all") { + input := &rolesanywhere.UpdateProfileInput{ + ProfileId: aws.String(d.Id()), + } + + if d.HasChange("duration_seconds") { + input.DurationSeconds = aws.Int32(int32(d.Get("duration_seconds").(int))) + } + + if d.HasChange("managed_policy_arns") { + input.ManagedPolicyArns = expandStringList(d.Get("managed_policy_arns").(*schema.Set).List()) + } + + if d.HasChange("name") { + input.Name = aws.String(d.Get("name").(string)) + } + + if d.HasChange("role_arns") { + input.RoleArns = expandStringList(d.Get("role_arns").(*schema.Set).List()) + } + + if d.HasChange("session_policy") { + input.Name = aws.String(d.Get("session_policy").(string)) + } + + log.Printf("[DEBUG] Updating RolesAnywhere Profile (%s): %#v", d.Id(), input) + _, err := conn.UpdateProfile(ctx, input) + if err != nil { + return diag.Errorf("updating RolesAnywhere Profile (%s): %s", d.Id(), err) + } + } + + if d.HasChange("enabled") { + _, n := d.GetChange("enabled") + if n == "true" { + err := enableProfile(ctx, d.Id(), meta) + if err != nil { + diag.Errorf("enabling RolesAnywhere Profile (%s): %s", d.Id(), err) + } + } else { + err := disableProfile(ctx, d.Id(), meta) + if err != nil { + diag.Errorf("disabling RolesAnywhere Profile (%s): %s", d.Id(), err) + } + } + } + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + if err := UpdateTags(ctx, conn, d.Get("arn").(string), o, n); err != nil { + return diag.Errorf("updating tags: %s", err) + } + } + + return resourceProfileRead(ctx, d, meta) +} + +func resourceProfileDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).RolesAnywhereConn + + log.Printf("[DEBUG] Deleting RolesAnywhere Profile (%s)", d.Id()) + _, err := conn.DeleteProfile(ctx, &rolesanywhere.DeleteProfileInput{ + ProfileId: aws.String(d.Id()), + }) + + var resourceNotFoundException *types.ResourceNotFoundException + if errors.As(err, &resourceNotFoundException) { + return nil + } + + if err != nil { + return diag.Errorf("deleting RolesAnywhere Profile: (%s): %s", d.Id(), err) + } + + return nil +} + +func disableProfile(ctx context.Context, profileId string, meta interface{}) error { + conn := meta.(*conns.AWSClient).RolesAnywhereConn + + input := &rolesanywhere.DisableProfileInput{ + ProfileId: aws.String(profileId), + } + + _, err := conn.DisableProfile(ctx, input) + return err +} + +func enableProfile(ctx context.Context, profileId string, meta interface{}) error { + conn := meta.(*conns.AWSClient).RolesAnywhereConn + + input := &rolesanywhere.EnableProfileInput{ + ProfileId: aws.String(profileId), + } + + _, err := conn.EnableProfile(ctx, input) + return err +} diff --git a/internal/service/rolesanywhere/profile_test.go b/internal/service/rolesanywhere/profile_test.go new file mode 100644 index 000000000000..4fc38d0e4828 --- /dev/null +++ b/internal/service/rolesanywhere/profile_test.go @@ -0,0 +1,227 @@ +package rolesanywhere_test + +import ( + "context" + "fmt" + "testing" + + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfrolesanywhere "github.com/hashicorp/terraform-provider-aws/internal/service/rolesanywhere" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccRolesAnywhereProfile_basic(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + roleName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_rolesanywhere_profile.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, names.RolesAnywhereEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckProfileDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProfileConfig_basic(rName, roleName), + Check: resource.ComposeTestCheckFunc( + testAccCheckProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "role_arns.#", "1"), + acctest.CheckResourceAttrGlobalARN(resourceName, "role_arns.0", "iam", fmt.Sprintf("role/%s", roleName)), + resource.TestCheckResourceAttr(resourceName, "duration_seconds", "3600"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccRolesAnywhereProfile_tags(t *testing.T) { + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + roleName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_rolesanywhere_profile.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, names.RolesAnywhereEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckProfileDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProfileConfig_tags1(rName, roleName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccProfileConfig_tags2(rName, roleName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccProfileConfig_tags1(rName, roleName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccRolesAnywhereProfile_disappears(t *testing.T) { + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + roleName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_rolesanywhere_profile.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, names.RolesAnywhereEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckProfileDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProfileConfig_basic(rName, roleName), + Check: resource.ComposeTestCheckFunc( + testAccCheckProfileExists(resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tfrolesanywhere.ResourceProfile(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckProfileDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).RolesAnywhereConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_rolesanywhere_profile" { + continue + } + + _, err := tfrolesanywhere.FindProfileByID(context.TODO(), conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("RolesAnywhere Profile %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccCheckProfileExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Profile is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).RolesAnywhereConn + + _, err := tfrolesanywhere.FindProfileByID(context.TODO(), conn, rs.Primary.ID) + + if err != nil { + return fmt.Errorf("Error describing Profile: %s", err.Error()) + } + + return nil + } +} + +func testAccProfileConfig_base(roleName string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_iam_role" "test" { + name = %[1]q + path = "/" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole", + Principal = { + Service = "ec2.${data.aws_partition.current.dns_suffix}", + } + Effect = "Allow" + Sid = "" + }] + }) +} +`, roleName) +} + +func testAccProfileConfig_basic(rName, roleName string) string { + return acctest.ConfigCompose( + testAccProfileConfig_base(roleName), + fmt.Sprintf(` +resource "aws_rolesanywhere_profile" "test" { + name = %[1]q + role_arns = [aws_iam_role.test.arn] +} +`, rName)) +} + +func testAccProfileConfig_tags1(rName, roleName, tag, value string) string { + return acctest.ConfigCompose( + testAccProfileConfig_base(roleName), + fmt.Sprintf(` +resource "aws_rolesanywhere_profile" "test" { + name = %[1]q + role_arns = [aws_iam_role.test.arn] + tags = { + %[2]q = %[3]q + } +} +`, rName, tag, value)) +} + +func testAccProfileConfig_tags2(rName, roleName, tag1, value1, tag2, value2 string) string { + return acctest.ConfigCompose( + testAccProfileConfig_base(roleName), + fmt.Sprintf(` +resource "aws_rolesanywhere_profile" "test" { + name = %[1]q + role_arns = [aws_iam_role.test.arn] + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tag1, value1, tag2, value2)) +} diff --git a/website/docs/r/rolesanywhere_profile.html.markdown b/website/docs/r/rolesanywhere_profile.html.markdown new file mode 100644 index 000000000000..6360f503ae66 --- /dev/null +++ b/website/docs/r/rolesanywhere_profile.html.markdown @@ -0,0 +1,69 @@ +--- +subcategory: "Roles Anywhere" +layout: "aws" +page_title: "AWS: aws_rolesanywhere_profile" +description: |- + Provides a Roles Anywhere Profile resource +--- + +# Resource: aws_rolesanywhere_profile + +Terraform resource for managing a Roles Anywhere Profile. + +## Example Usage + +```terraform +data "aws_partition" "current" {} + +resource "aws_iam_role" "test" { + name = "test" + path = "/" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole", + Principal = { + Service = "ec2.${data.aws_partition.current.dns_suffix}", + } + Effect = "Allow" + Sid = "" + }] + }) +} + +resource "aws_rolesanywhere_profile" "test" { + + name = "example" + role_arns = [aws_iam_role.test.arn] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `duration_seconds` - (Optional) The number of seconds the vended session credentials are valid for. Defaults to 3600. +* `enabled` - (Optional) Whether or not the Profile is enabled. +* `managed_policy_arns` - (Optional) A list of managed policy ARNs that apply to the vended session credentials. +* `name` - (Required) The name of the Profile. +* `require_instance_properties` - (Optional) Specifies whether instance properties are required in [CreateSession](https://docs.aws.amazon.com/rolesanywhere/latest/APIReference/API_CreateSession.html) requests with this profile. +* `role_arns` - (Required) A list of IAM roles that this profile can assume +* `session_policy` - (Optional) A session policy that applies to the trust boundary of the vended session credentials. +* `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - Amazon Resource Name (ARN) of the Profile +* `id` - The Profile ID. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). + +## Import + +`aws_rolesanywhere_profile` can be imported using its `id`, e.g. + +``` +$ terraform import aws_rolesanywhere_profile.example db138a85-8925-4f9f-a409-08231233cacf +``` From 22ca52ea74c34299202cb4f6b87a142862e9958c Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 19 Jul 2022 09:22:53 -0400 Subject: [PATCH 2/2] Update profile.go --- internal/service/rolesanywhere/profile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/rolesanywhere/profile.go b/internal/service/rolesanywhere/profile.go index ebc5502b31f4..44a99d9f6a8d 100644 --- a/internal/service/rolesanywhere/profile.go +++ b/internal/service/rolesanywhere/profile.go @@ -105,7 +105,7 @@ func resourceProfileCreate(ctx context.Context, d *schema.ResourceData, meta int input.SessionPolicy = aws.String(v.(string)) } - log.Printf("[DEBUG] Creating RolesAnywhere Profile (%s): %#v", d.Id(), input) + log.Printf("[DEBUG] Creating RolesAnywhere Profile: %#v", input) output, err := conn.CreateProfile(ctx, input) if err != nil {