diff --git a/.changelog/22140.txt b/.changelog/22140.txt new file mode 100644 index 00000000000..02403db6d69 --- /dev/null +++ b/.changelog/22140.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_emr_studio_session_mapping +``` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index cf90a91b7e7..ed0ffe266e2 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1195,6 +1195,7 @@ func Provider() *schema.Provider { "aws_emr_managed_scaling_policy": emr.ResourceManagedScalingPolicy(), "aws_emr_security_configuration": emr.ResourceSecurityConfiguration(), "aws_emr_studio": emr.ResourceStudio(), + "aws_emr_studio_session_mapping": emr.ResourceStudioSessionMapping(), "aws_kinesis_firehose_delivery_stream": firehose.ResourceDeliveryStream(), diff --git a/internal/service/emr/find.go b/internal/service/emr/find.go index 9c169e232ce..76d4e780ce2 100644 --- a/internal/service/emr/find.go +++ b/internal/service/emr/find.go @@ -74,3 +74,36 @@ func FindStudioByID(conn *emr.EMR, id string) (*emr.Studio, error) { return output.Studio, nil } + +func FindStudioSessionMappingByID(conn *emr.EMR, id string) (*emr.SessionMappingDetail, error) { + studioId, identityType, identityId, err := readStudioSessionMapping(id) + if err != nil { + return nil, err + } + + input := &emr.GetStudioSessionMappingInput{ + StudioId: aws.String(studioId), + IdentityType: aws.String(identityType), + IdentityId: aws.String(identityId), + } + + output, err := conn.GetStudioSessionMapping(input) + + if tfawserr.ErrMessageContains(err, emr.ErrCodeInvalidRequestException, "Studio session mapping does not exist") || + tfawserr.ErrMessageContains(err, emr.ErrCodeInvalidRequestException, "Studio does not exist") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.SessionMapping == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.SessionMapping, nil +} diff --git a/internal/service/emr/id.go b/internal/service/emr/id.go new file mode 100644 index 00000000000..a2789f382f9 --- /dev/null +++ b/internal/service/emr/id.go @@ -0,0 +1,14 @@ +package emr + +import ( + "fmt" + "strings" +) + +func readStudioSessionMapping(id string) (studioId, identityType, identityId string, err error) { + idParts := strings.Split(id, ":") + if len(idParts) != 3 { + return "", "", "", fmt.Errorf("expected ID in format studio-id:identity-type:identity-id, received: %s", id) + } + return idParts[0], idParts[1], idParts[2], nil +} diff --git a/internal/service/emr/studio_session_mapping.go b/internal/service/emr/studio_session_mapping.go new file mode 100644 index 00000000000..a71397e2d44 --- /dev/null +++ b/internal/service/emr/studio_session_mapping.go @@ -0,0 +1,164 @@ +package emr + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/emr" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +func ResourceStudioSessionMapping() *schema.Resource { + return &schema.Resource{ + Create: resourceStudioSessionMappingCreate, + Read: resourceStudioSessionMappingRead, + Update: resourceStudioSessionMappingUpdate, + Delete: resourceStudioSessionMappingDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "identity_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ExactlyOneOf: []string{"identity_id", "identity_name"}, + }, + "identity_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ExactlyOneOf: []string{"identity_id", "identity_name"}, + }, + "identity_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(emr.IdentityType_Values(), false), + }, + "session_policy_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidARN, + }, + "studio_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceStudioSessionMappingCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EMRConn + + var id string + studioId := d.Get("studio_id").(string) + identityType := d.Get("identity_type").(string) + input := &emr.CreateStudioSessionMappingInput{ + IdentityType: aws.String(identityType), + SessionPolicyArn: aws.String(d.Get("session_policy_arn").(string)), + StudioId: aws.String(studioId), + } + + if v, ok := d.GetOk("identity_id"); ok { + input.IdentityId = aws.String(v.(string)) + id = v.(string) + } + + if v, ok := d.GetOk("identity_name"); ok { + input.IdentityName = aws.String(v.(string)) + id = v.(string) + } + + _, err := conn.CreateStudioSessionMapping(input) + if err != nil { + return fmt.Errorf("error creating EMR Studio Session Mapping: %w", err) + } + + d.SetId(fmt.Sprintf("%s:%s:%s", studioId, identityType, id)) + + return resourceStudioSessionMappingRead(d, meta) +} + +func resourceStudioSessionMappingUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EMRConn + + studioId, identityType, identityId, err := readStudioSessionMapping(d.Id()) + if err != nil { + return err + } + + input := &emr.UpdateStudioSessionMappingInput{ + SessionPolicyArn: aws.String(d.Get("session_policy_arn").(string)), + IdentityType: aws.String(identityType), + StudioId: aws.String(studioId), + IdentityId: aws.String(identityId), + } + + _, err = conn.UpdateStudioSessionMapping(input) + if err != nil { + return fmt.Errorf("error updating EMR Studio Session Mapping: %w", err) + } + + return resourceStudioSessionMappingRead(d, meta) +} + +func resourceStudioSessionMappingRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EMRConn + + mapping, err := FindStudioSessionMappingByID(conn, d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] EMR Studio Session Mapping (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading EMR Studio Session Mapping (%s): %w", d.Id(), err) + } + + d.Set("identity_type", mapping.IdentityType) + d.Set("identity_id", mapping.IdentityId) + d.Set("identity_name", mapping.IdentityName) + d.Set("studio_id", mapping.StudioId) + d.Set("session_policy_arn", mapping.SessionPolicyArn) + + return nil +} + +func resourceStudioSessionMappingDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EMRConn + studioId, identityType, identityId, err := readStudioSessionMapping(d.Id()) + if err != nil { + return err + } + + input := &emr.DeleteStudioSessionMappingInput{ + IdentityType: aws.String(identityType), + StudioId: aws.String(studioId), + IdentityId: aws.String(identityId), + } + + log.Printf("[INFO] Deleting EMR Studio Session Mapping: %s", d.Id()) + _, err = conn.DeleteStudioSessionMapping(input) + + if err != nil { + if tfawserr.ErrMessageContains(err, emr.ErrCodeInvalidRequestException, "Studio session mapping does not exist.") { + return nil + } + return fmt.Errorf("error deleting EMR Studio Session Mapping (%s): %w", d.Id(), err) + } + + return nil +} diff --git a/internal/service/emr/studio_session_mapping_test.go b/internal/service/emr/studio_session_mapping_test.go new file mode 100644 index 00000000000..72985a5e120 --- /dev/null +++ b/internal/service/emr/studio_session_mapping_test.go @@ -0,0 +1,285 @@ +package emr_test + +import ( + "fmt" + "os" + "testing" + + "github.com/aws/aws-sdk-go/service/emr" + 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" + tfemr "github.com/hashicorp/terraform-provider-aws/internal/service/emr" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccEMRStudioSessionMapping_basic(t *testing.T) { + var studio emr.SessionMappingDetail + resourceName := "aws_emr_studio_session_mapping.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + updatedName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + uName := os.Getenv("AWS_IDENTITY_STORE_USER_ID") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + testAccPreCheckUserID(t) + }, + ErrorCheck: acctest.ErrorCheck(t, emr.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckEmrStudioSessionMappingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccEMRStudioSessionMappingConfigBasic(rName, uName), + Check: resource.ComposeTestCheckFunc( + testAccCheckEmrStudioSessionMappingExists(resourceName, &studio), + resource.TestCheckResourceAttr(resourceName, "identity_id", uName), + resource.TestCheckResourceAttr(resourceName, "identity_type", "USER"), + resource.TestCheckResourceAttrPair(resourceName, "studio_id", "aws_emr_studio.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "session_policy_arn", "aws_iam_policy.test", "arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccEMRStudioSessionMappingConfigUpdated(rName, uName, updatedName), + Check: resource.ComposeTestCheckFunc( + testAccCheckEmrStudioSessionMappingExists(resourceName, &studio), + resource.TestCheckResourceAttr(resourceName, "identity_id", uName), + resource.TestCheckResourceAttr(resourceName, "identity_type", "USER"), + resource.TestCheckResourceAttrPair(resourceName, "studio_id", "aws_emr_studio.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "session_policy_arn", "aws_iam_policy.test2", "arn"), + ), + }, + }, + }) +} + +func TestAccEMRStudioSessionMapping_disappears(t *testing.T) { + var studio emr.SessionMappingDetail + resourceName := "aws_emr_studio_session_mapping.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + uName := os.Getenv("AWS_IDENTITY_STORE_USER_ID") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + testAccPreCheckUserID(t) + }, + ErrorCheck: acctest.ErrorCheck(t, emr.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckEmrStudioSessionMappingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccEMRStudioSessionMappingConfigBasic(rName, uName), + Check: resource.ComposeTestCheckFunc( + testAccCheckEmrStudioSessionMappingExists(resourceName, &studio), + acctest.CheckResourceDisappears(acctest.Provider, tfemr.ResourceStudioSessionMapping(), resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tfemr.ResourceStudioSessionMapping(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckEmrStudioSessionMappingExists(resourceName string, studio *emr.SessionMappingDetail) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).EMRConn + + output, err := tfemr.FindStudioSessionMappingByID(conn, rs.Primary.ID) + if err != nil { + return err + } + + if output == nil { + return fmt.Errorf("EMR Studio (%s) not found", rs.Primary.ID) + } + + *studio = *output + + return nil + } +} + +func testAccCheckEmrStudioSessionMappingDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).EMRConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_emr_studio_session_mapping" { + continue + } + + _, err := tfemr.FindStudioSessionMappingByID(conn, rs.Primary.ID) + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("EMR Studio %s still exists", rs.Primary.ID) + } + return nil +} + +func testAccPreCheckUserID(t *testing.T) { + if os.Getenv("AWS_IDENTITY_STORE_USER_ID") == "" { + t.Skip("AWS_IDENTITY_STORE_USER_ID env var must be set for AWS Identity Store User acceptance test. " + + "This is required until ListUsers API returns results without filtering by name.") + } +} + +func testAccEMRStudioSessionMappingConfigBase(rName string) string { + return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "test" { + vpc_id = aws_vpc.test.id + cidr_block = "10.0.1.0/24" + availability_zone = data.aws_availability_zones.available.names[0] +} + +resource "aws_s3_bucket" "test" { + bucket = %[1]q + acl = "private" + force_destroy = true +} + +resource "aws_iam_role" "test" { + name = %[1]q + path = "/" + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +data "aws_iam_policy_document" "assume_role" { + statement { + actions = ["sts:AssumeRole"] + effect = "Allow" + + principals { + type = "Service" + identifiers = ["elasticmapreduce.${data.aws_partition.current.dns_suffix}"] + } + } +} + +resource "aws_iam_role_policy" "test" { + name = %[1]q + role = aws_iam_role.test.id + policy = <