From 2f210fbadf5e1598262875e28ca51481a0d82ffa Mon Sep 17 00:00:00 2001 From: deepakbshetty Date: Fri, 26 Jul 2024 21:02:25 +0100 Subject: [PATCH 1/4] r/aws_sagemaker_userprofile: add studio_web_portal_settings --- .changelog/38567.txt | 3 + internal/service/sagemaker/sagemaker_test.go | 2 + internal/service/sagemaker/user_profile.go | 25 ++++ .../service/sagemaker/user_profile_test.go | 132 ++++++++++++++++++ .../r/sagemaker_user_profile.html.markdown | 6 + 5 files changed, 168 insertions(+) create mode 100644 .changelog/38567.txt diff --git a/.changelog/38567.txt b/.changelog/38567.txt new file mode 100644 index 00000000000..c44f2352255 --- /dev/null +++ b/.changelog/38567.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_sagemaker_user_profile: Add `user_settings.studio_web_portal_settings` block +``` \ No newline at end of file diff --git a/internal/service/sagemaker/sagemaker_test.go b/internal/service/sagemaker/sagemaker_test.go index 9550a5ccc6d..a79ab6f890e 100644 --- a/internal/service/sagemaker/sagemaker_test.go +++ b/internal/service/sagemaker/sagemaker_test.go @@ -106,6 +106,8 @@ func TestAccSageMaker_serial(t *testing.T) { "kernelGatewayAppSettings_imageConfig": testAccUserProfile_kernelGatewayAppSettings_imageconfig, "codeEditorAppSettings_customImage": testAccUserProfile_codeEditorAppSettings_customImage, "jupyterServerAppSettings": testAccUserProfile_jupyterServerAppSettings, + "studioWebPortalSettings_hiddenAppTypes": testAccUserProfile_studioWebPortalSettings_hiddenAppTypes, + "studioWebPortalSettings_hiddenMlTools": testAccUserProfile_studioWebPortalSettings_hiddenMlTools, }, "Workforce": { acctest.CtDisappears: testAccWorkforce_disappears, diff --git a/internal/service/sagemaker/user_profile.go b/internal/service/sagemaker/user_profile.go index f82ea57d48a..05604c6ba00 100644 --- a/internal/service/sagemaker/user_profile.go +++ b/internal/service/sagemaker/user_profile.go @@ -740,6 +740,31 @@ func resourceUserProfile() *schema.Resource { }, }, }, + "studio_web_portal_settings": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "hidden_app_types": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: enum.Validate[awstypes.AppType](), + }, + }, + "hidden_ml_tools": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: enum.Validate[awstypes.MlTools](), + }, + }, + }, + }, + }, }, }, }, diff --git a/internal/service/sagemaker/user_profile_test.go b/internal/service/sagemaker/user_profile_test.go index bf00b64d29d..c1e492d91ad 100644 --- a/internal/service/sagemaker/user_profile_test.go +++ b/internal/service/sagemaker/user_profile_test.go @@ -345,6 +345,90 @@ func testAccUserProfile_jupyterServerAppSettings(t *testing.T) { }) } +func testAccUserProfile_studioWebPortalSettings_hiddenAppTypes(t *testing.T) { + ctx := acctest.Context(t) + var domain sagemaker.DescribeUserProfileOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_sagemaker_user_profile.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.SageMakerServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckUserProfileDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccUserProfileConfig_studioWebPortalSettings_hiddenAppTypes(rName, []string{"JupyterServer", "KernelGateway"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserProfileExists(ctx, resourceName, &domain), + resource.TestCheckResourceAttr(resourceName, "user_settings.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "user_settings.0.studio_web_portal_settings.#", acctest.Ct1), + resource.TestCheckTypeSetElemAttr(resourceName, "user_settings.0.studio_web_portal_settings.0.hidden_app_types.*", "JupyterServer"), + resource.TestCheckTypeSetElemAttr(resourceName, "user_settings.0.studio_web_portal_settings.0.hidden_app_types.*", "KernelGateway"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccUserProfileConfig_studioWebPortalSettings_hiddenAppTypes(rName, []string{"JupyterServer", "KernelGateway", "CodeEditor"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserProfileExists(ctx, resourceName, &domain), + resource.TestCheckResourceAttr(resourceName, "user_settings.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "user_settings.0.studio_web_portal_settings.#", acctest.Ct1), + resource.TestCheckTypeSetElemAttr(resourceName, "user_settings.0.studio_web_portal_settings.0.hidden_app_types.*", "JupyterServer"), + resource.TestCheckTypeSetElemAttr(resourceName, "user_settings.0.studio_web_portal_settings.0.hidden_app_types.*", "KernelGateway"), + resource.TestCheckTypeSetElemAttr(resourceName, "user_settings.0.studio_web_portal_settings.0.hidden_app_types.*", "CodeEditor"), + ), + }, + }, + }) +} + +func testAccUserProfile_studioWebPortalSettings_hiddenMlTools(t *testing.T) { + ctx := acctest.Context(t) + var domain sagemaker.DescribeUserProfileOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_sagemaker_user_profile.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.SageMakerServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckUserProfileDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccUserProfileConfig_studioWebPortalSettings_hiddenMlTools(rName, []string{"DataWrangler", "FeatureStore"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserProfileExists(ctx, resourceName, &domain), + resource.TestCheckResourceAttr(resourceName, "user_settings.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "user_settings.0.studio_web_portal_settings.#", acctest.Ct1), + resource.TestCheckTypeSetElemAttr(resourceName, "user_settings.0.studio_web_portal_settings.0.hidden_ml_tools.*", "DataWrangler"), + resource.TestCheckTypeSetElemAttr(resourceName, "user_settings.0.studio_web_portal_settings.0.hidden_ml_tools.*", "FeatureStore"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccUserProfileConfig_studioWebPortalSettings_hiddenMlTools(rName, []string{"DataWrangler", "FeatureStore", "EmrClusters"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserProfileExists(ctx, resourceName, &domain), + resource.TestCheckResourceAttr(resourceName, "user_settings.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "user_settings.0.studio_web_portal_settings.#", acctest.Ct1), + resource.TestCheckTypeSetElemAttr(resourceName, "user_settings.0.studio_web_portal_settings.0.hidden_ml_tools.*", "DataWrangler"), + resource.TestCheckTypeSetElemAttr(resourceName, "user_settings.0.studio_web_portal_settings.0.hidden_ml_tools.*", "FeatureStore"), + resource.TestCheckTypeSetElemAttr(resourceName, "user_settings.0.studio_web_portal_settings.0.hidden_ml_tools.*", "EmrClusters"), + ), + }, + }, + }) +} + func testAccUserProfile_disappears(t *testing.T) { ctx := acctest.Context(t) var domain sagemaker.DescribeUserProfileOutput @@ -692,3 +776,51 @@ resource "aws_sagemaker_user_profile" "test" { } `, rName, baseImage)) } + +func testAccUserProfileConfig_studioWebPortalSettings_hiddenAppTypes(rName string, hiddenAppTypes []string) string { + var hiddenAppTypesString string + for i, appType := range hiddenAppTypes { + if i > 0 { + hiddenAppTypesString += ", " + } + hiddenAppTypesString += fmt.Sprintf("%q", appType) + } + return acctest.ConfigCompose(testAccUserProfileConfig_base(rName), fmt.Sprintf(` +resource "aws_sagemaker_user_profile" "test" { + domain_id = aws_sagemaker_domain.test.id + user_profile_name = %[1]q + + user_settings { + execution_role = aws_iam_role.test.arn + + studio_web_portal_settings { + hidden_app_types = [%[2]s] + } + } +} +`, rName, hiddenAppTypesString)) +} + +func testAccUserProfileConfig_studioWebPortalSettings_hiddenMlTools(rName string, hiddenMlTools []string) string { + var hiddenMlToolsString string + for i, mlTool := range hiddenMlTools { + if i > 0 { + hiddenMlToolsString += ", " + } + hiddenMlToolsString += fmt.Sprintf("%q", mlTool) + } + return acctest.ConfigCompose(testAccUserProfileConfig_base(rName), fmt.Sprintf(` +resource "aws_sagemaker_user_profile" "test" { + domain_id = aws_sagemaker_domain.test.id + user_profile_name = %[1]q + + user_settings { + execution_role = aws_iam_role.test.arn + + studio_web_portal_settings { + hidden_ml_tools = [%[2]s] + } + } +} +`, rName, hiddenMlToolsString)) +} diff --git a/website/docs/r/sagemaker_user_profile.html.markdown b/website/docs/r/sagemaker_user_profile.html.markdown index c062db7b8ca..7aba76d7bd9 100644 --- a/website/docs/r/sagemaker_user_profile.html.markdown +++ b/website/docs/r/sagemaker_user_profile.html.markdown @@ -50,6 +50,7 @@ This resource supports the following arguments: * `space_storage_settings` - (Optional) The storage settings for a private space. See [Space Storage Settings](#space_storage_settings) below. * `studio_web_portal` - (Optional) Whether the user can access Studio. If this value is set to `DISABLED`, the user cannot access Studio, even if that is the default experience for the domain. Valid values are `ENABLED` and `DISABLED`. * `tensor_board_app_settings` - (Optional) The TensorBoard app settings. See [TensorBoard App Settings](#tensor_board_app_settings) below. +* `studio_web_portal_settings` - (Optional) The Studio Web Portal settings. See [`studio_web_portal_settings` Block](#studio_web_portal_settings-block) below. #### space_storage_settings @@ -111,6 +112,11 @@ This resource supports the following arguments: * `access_status` - (Optional) Indicates whether the current user has access to the RStudioServerPro app. Valid values are `ENABLED` and `DISABLED`. * `user_group` - (Optional) The level of permissions that the user has within the RStudioServerPro app. This value defaults to `R_STUDIO_USER`. The `R_STUDIO_ADMIN` value allows the user access to the RStudio Administrative Dashboard. Valid values are `R_STUDIO_USER` and `R_STUDIO_ADMIN`. +#### `studio_web_portal_settings` Block + +* `hidden_app_types` - (Optional) The Applications supported in Studio that are hidden from the Studio left navigation pane. +* `hidden_ml_tools` - (Optional) The machine learning tools that are hidden from the Studio left navigation pane. + ##### code_repository * `repository_url` - (Optional) The URL of the Git repository. From bec98f7f42ac1ee593db7f762d5f88c09d0e65cc Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 9 Sep 2024 11:50:42 -0400 Subject: [PATCH 2/4] Cosmetics. --- .changelog/38567.txt | 2 +- internal/service/sagemaker/user_profile.go | 79 +++++++++++----------- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/.changelog/38567.txt b/.changelog/38567.txt index c44f2352255..4f83d03ebdb 100644 --- a/.changelog/38567.txt +++ b/.changelog/38567.txt @@ -1,3 +1,3 @@ ```release-note:enhancement -resource/aws_sagemaker_user_profile: Add `user_settings.studio_web_portal_settings` block +resource/aws_sagemaker_user_profile: Add `user_settings.studio_web_portal_settings` configuration block ``` \ No newline at end of file diff --git a/internal/service/sagemaker/user_profile.go b/internal/service/sagemaker/user_profile.go index 05604c6ba00..97232821cfb 100644 --- a/internal/service/sagemaker/user_profile.go +++ b/internal/service/sagemaker/user_profile.go @@ -36,6 +36,7 @@ func resourceUserProfile() *schema.Resource { ReadWithoutTimeout: resourceUserProfileRead, UpdateWithoutTimeout: resourceUserProfileUpdate, DeleteWithoutTimeout: resourceUserProfileDelete, + Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, @@ -45,19 +46,14 @@ func resourceUserProfile() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "user_profile_name": { + "domain_id": { Type: schema.TypeString, Required: true, ForceNew: true, - ValidateFunc: validation.All( - validation.StringLenBetween(1, 63), - validation.StringMatch(regexache.MustCompile(`^[0-9A-Za-z](-*[0-9A-Za-z]){0,62}`), "Valid characters are a-z, A-Z, 0-9, and - (hyphen)."), - ), }, - "domain_id": { + "home_efs_file_system_uid": { Type: schema.TypeString, - Required: true, - ForceNew: true, + Computed: true, }, "single_sign_on_user_identifier": { Type: schema.TypeString, @@ -69,6 +65,17 @@ func resourceUserProfile() *schema.Resource { Optional: true, ForceNew: true, }, + names.AttrTags: tftags.TagsSchema(), + names.AttrTagsAll: tftags.TagsSchemaComputed(), + "user_profile_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 63), + validation.StringMatch(regexache.MustCompile(`^[0-9A-Za-z](-*[0-9A-Za-z]){0,62}`), "Valid characters are a-z, A-Z, 0-9, and - (hyphen)."), + ), + }, "user_settings": { Type: schema.TypeList, Optional: true, @@ -698,6 +705,31 @@ func resourceUserProfile() *schema.Resource { }, }, }, + "studio_web_portal_settings": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "hidden_app_types": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: enum.Validate[awstypes.AppType](), + }, + }, + "hidden_ml_tools": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: enum.Validate[awstypes.MlTools](), + }, + }, + }, + }, + }, "tensor_board_app_settings": { Type: schema.TypeList, Optional: true, @@ -740,40 +772,9 @@ func resourceUserProfile() *schema.Resource { }, }, }, - "studio_web_portal_settings": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "hidden_app_types": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateDiagFunc: enum.Validate[awstypes.AppType](), - }, - }, - "hidden_ml_tools": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateDiagFunc: enum.Validate[awstypes.MlTools](), - }, - }, - }, - }, - }, }, }, }, - names.AttrTags: tftags.TagsSchema(), - names.AttrTagsAll: tftags.TagsSchemaComputed(), - "home_efs_file_system_uid": { - Type: schema.TypeString, - Computed: true, - }, }, CustomizeDiff: verify.SetTagsDiff, From 78aaaeb4e5bbaec262e6bbbb05eb64aa64d3e84e Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 9 Sep 2024 12:50:57 -0400 Subject: [PATCH 3/4] Add 'expandStudioWebPortalSettings' and 'flattenStudioWebPortalSettings'. --- internal/service/sagemaker/domain.go | 46 +++++++ internal/service/sagemaker/status.go | 16 --- internal/service/sagemaker/user_profile.go | 148 +++++++++++++++------ internal/service/sagemaker/wait.go | 44 ------ 4 files changed, 153 insertions(+), 101 deletions(-) diff --git a/internal/service/sagemaker/domain.go b/internal/service/sagemaker/domain.go index 6b2658b18a2..9fda75531b4 100644 --- a/internal/service/sagemaker/domain.go +++ b/internal/service/sagemaker/domain.go @@ -1379,6 +1379,10 @@ func expandUserSettings(l []interface{}) *awstypes.UserSettings { config.RStudioServerProAppSettings = expandRStudioServerProAppSettings(v) } + if v, ok := m["studio_web_portal_settings"].([]interface{}); ok && len(v) > 0 { + config.StudioWebPortalSettings = expandStudioWebPortalSettings(v) + } + return config } @@ -1839,6 +1843,26 @@ func expandDomainCustomImages(l []interface{}) []awstypes.CustomImage { return images } +func expandStudioWebPortalSettings(l []interface{}) *awstypes.StudioWebPortalSettings { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &awstypes.StudioWebPortalSettings{} + + if v, ok := m["hidden_app_types"].(*schema.Set); ok && v.Len() > 0 { + config.HiddenAppTypes = flex.ExpandStringyValueSet[awstypes.AppType](v) + } + + if v, ok := m["hidden_ml_tools"].(*schema.Set); ok && v.Len() > 0 { + config.HiddenMlTools = flex.ExpandStringyValueSet[awstypes.MlTools](v) + } + + return config +} + func flattenUserSettings(config *awstypes.UserSettings) []map[string]interface{} { if config == nil { return []map[string]interface{}{} @@ -1908,6 +1932,10 @@ func flattenUserSettings(config *awstypes.UserSettings) []map[string]interface{} m["r_studio_server_pro_app_settings"] = flattenRStudioServerProAppSettings(config.RStudioServerProAppSettings) } + if config.StudioWebPortalSettings != nil { + m["studio_web_portal_settings"] = flattenStudioWebPortalSettings(config.StudioWebPortalSettings) + } + return []map[string]interface{}{m} } @@ -2508,3 +2536,21 @@ func flattenEFSFileSystemConfig(apiObject awstypes.EFSFileSystemConfig) []map[st return []map[string]interface{}{tfMap} } + +func flattenStudioWebPortalSettings(config *awstypes.StudioWebPortalSettings) []map[string]interface{} { + if config == nil { + return []map[string]interface{}{} + } + + m := map[string]interface{}{} + + if config.HiddenAppTypes != nil { + m["hidden_app_types"] = flex.FlattenStringyValueSet[awstypes.AppType](config.HiddenAppTypes) + } + + if config.HiddenMlTools != nil { + m["hidden_ml_tools"] = flex.FlattenStringyValueSet[awstypes.MlTools](config.HiddenMlTools) + } + + return []map[string]interface{}{m} +} diff --git a/internal/service/sagemaker/status.go b/internal/service/sagemaker/status.go index de7a24736e4..69fd2adc111 100644 --- a/internal/service/sagemaker/status.go +++ b/internal/service/sagemaker/status.go @@ -123,22 +123,6 @@ func statusFlowDefinition(ctx context.Context, conn *sagemaker.Client, name stri } } -func statusUserProfile(ctx context.Context, conn *sagemaker.Client, domainID, userProfileName string) retry.StateRefreshFunc { - return func() (interface{}, string, error) { - output, err := findUserProfileByName(ctx, conn, domainID, userProfileName) - - if tfresource.NotFound(err) { - return nil, "", nil - } - - if err != nil { - return nil, "", err - } - - return output, string(output.Status), nil - } -} - func statusApp(ctx context.Context, conn *sagemaker.Client, domainID, userProfileOrSpaceName, appType, appName string) retry.StateRefreshFunc { return func() (interface{}, string, error) { output, err := findAppByName(ctx, conn, domainID, userProfileOrSpaceName, appType, appName) diff --git a/internal/service/sagemaker/user_profile.go b/internal/service/sagemaker/user_profile.go index 97232821cfb..983544d2ae8 100644 --- a/internal/service/sagemaker/user_profile.go +++ b/internal/service/sagemaker/user_profile.go @@ -5,9 +5,11 @@ package sagemaker import ( "context" + "errors" "fmt" "log" "strings" + "time" "github.com/YakDriver/regexache" "github.com/aws/aws-sdk-go-v2/aws" @@ -785,10 +787,11 @@ func resourceUserProfileCreate(ctx context.Context, d *schema.ResourceData, meta var diags diag.Diagnostics conn := meta.(*conns.AWSClient).SageMakerClient(ctx) + name := d.Get("user_profile_name").(string) input := &sagemaker.CreateUserProfileInput{ - UserProfileName: aws.String(d.Get("user_profile_name").(string)), DomainId: aws.String(d.Get("domain_id").(string)), Tags: getTagsIn(ctx), + UserProfileName: aws.String(name), } if v, ok := d.GetOk("user_settings"); ok { @@ -803,22 +806,22 @@ func resourceUserProfileCreate(ctx context.Context, d *schema.ResourceData, meta input.SingleSignOnUserValue = aws.String(v.(string)) } - log.Printf("[DEBUG] SageMaker User Profile create config: %#v", *input) output, err := conn.CreateUserProfile(ctx, input) + if err != nil { - return sdkdiag.AppendErrorf(diags, "creating SageMaker User Profile: %s", err) + return sdkdiag.AppendErrorf(diags, "creating SageMaker User Profile (%s): %s", name, err) } - userProfileArn := aws.ToString(output.UserProfileArn) - domainID, userProfileName, err := decodeUserProfileName(userProfileArn) + userProfileARN := aws.ToString(output.UserProfileArn) + domainID, userProfileName, err := decodeUserProfileName(userProfileARN) if err != nil { - return sdkdiag.AppendErrorf(diags, "creating SageMaker User Profile: %s", err) + return sdkdiag.AppendFromErr(diags, err) } - d.SetId(userProfileArn) + d.SetId(userProfileARN) - if err := waitUserProfileInService(ctx, conn, domainID, userProfileName); err != nil { - return sdkdiag.AppendErrorf(diags, "creating SageMaker User Profile (%s): waiting for completion: %s", d.Id(), err) + if _, err := waitUserProfileInService(ctx, conn, domainID, userProfileName); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for SageMaker User Profile (%s) create: %s", d.Id(), err) } return append(diags, resourceUserProfileRead(ctx, d, meta)...) @@ -830,7 +833,7 @@ func resourceUserProfileRead(ctx context.Context, d *schema.ResourceData, meta i domainID, userProfileName, err := decodeUserProfileName(d.Id()) if err != nil { - return sdkdiag.AppendErrorf(diags, "reading SageMaker User Profile (%s): %s", d.Id(), err) + return sdkdiag.AppendFromErr(diags, err) } userProfile, err := findUserProfileByName(ctx, conn, domainID, userProfileName) @@ -851,9 +854,8 @@ func resourceUserProfileRead(ctx context.Context, d *schema.ResourceData, meta i d.Set("single_sign_on_user_identifier", userProfile.SingleSignOnUserIdentifier) d.Set("single_sign_on_user_value", userProfile.SingleSignOnUserValue) d.Set("user_profile_name", userProfile.UserProfileName) - if err := d.Set("user_settings", flattenUserSettings(userProfile.UserSettings)); err != nil { - return sdkdiag.AppendErrorf(diags, "setting user_settings for SageMaker User Profile (%s): %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "setting user_settings: %s", err) } return diags @@ -863,24 +865,26 @@ func resourceUserProfileUpdate(ctx context.Context, d *schema.ResourceData, meta var diags diag.Diagnostics conn := meta.(*conns.AWSClient).SageMakerClient(ctx) - if d.HasChange("user_settings") { - domainID := d.Get("domain_id").(string) - userProfileName := d.Get("user_profile_name").(string) + domainID, userProfileName, err := decodeUserProfileName(d.Id()) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + if d.HasChange("user_settings") { input := &sagemaker.UpdateUserProfileInput{ - UserProfileName: aws.String(userProfileName), DomainId: aws.String(domainID), + UserProfileName: aws.String(userProfileName), UserSettings: expandUserSettings(d.Get("user_settings").([]interface{})), } - log.Printf("[DEBUG] SageMaker User Profile update config: %#v", *input) _, err := conn.UpdateUserProfile(ctx, input) + if err != nil { - return sdkdiag.AppendErrorf(diags, "updating SageMaker User Profile: %s", err) + return sdkdiag.AppendErrorf(diags, "updating SageMaker User Profile (%s): %s", d.Id(), err) } - if err := waitUserProfileInService(ctx, conn, domainID, userProfileName); err != nil { - return sdkdiag.AppendErrorf(diags, "waiting for SageMaker User Profile (%s) to update: %s", d.Id(), err) + if _, err := waitUserProfileInService(ctx, conn, domainID, userProfileName); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for SageMaker User Profile (%s) update: %s", d.Id(), err) } } @@ -891,29 +895,50 @@ func resourceUserProfileDelete(ctx context.Context, d *schema.ResourceData, meta var diags diag.Diagnostics conn := meta.(*conns.AWSClient).SageMakerClient(ctx) - userProfileName := d.Get("user_profile_name").(string) - domainID := d.Get("domain_id").(string) + domainID, userProfileName, err := decodeUserProfileName(d.Id()) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } - input := &sagemaker.DeleteUserProfileInput{ + _, err = conn.DeleteUserProfile(ctx, &sagemaker.DeleteUserProfileInput{ UserProfileName: aws.String(userProfileName), DomainId: aws.String(domainID), + }) + + if errs.IsA[*awstypes.ResourceNotFound](err) { + return diags } - if _, err := conn.DeleteUserProfile(ctx, input); err != nil { - if !errs.IsA[*awstypes.ResourceNotFound](err) { - return sdkdiag.AppendErrorf(diags, "deleting SageMaker User Profile (%s): %s", d.Id(), err) - } + if err != nil { + return sdkdiag.AppendErrorf(diags, "deleting SageMaker User Profile (%s): %s", d.Id(), err) } if _, err := waitUserProfileDeleted(ctx, conn, domainID, userProfileName); err != nil { - if !errs.IsA[*awstypes.ResourceNotFound](err) { - return sdkdiag.AppendErrorf(diags, "waiting for SageMaker User Profile (%s) to delete: %s", d.Id(), err) - } + return sdkdiag.AppendErrorf(diags, "waiting for SageMaker User Profile (%s) delete: %s", d.Id(), err) } return diags } +func decodeUserProfileName(id string) (string, string, error) { + userProfileARN, err := arn.Parse(id) + if err != nil { + return "", "", err + } + + userProfileResourceNameName := strings.TrimPrefix(userProfileARN.Resource, "user-profile/") + parts := strings.Split(userProfileResourceNameName, "/") + + if len(parts) != 2 { + return "", "", fmt.Errorf("Unexpected format of ID (%q), expected DOMAIN-ID/USER-PROFILE-NAME", userProfileResourceNameName) + } + + domainID := parts[0] + userProfileName := parts[1] + + return domainID, userProfileName, nil +} + func findUserProfileByName(ctx context.Context, conn *sagemaker.Client, domainID, userProfileName string) (*sagemaker.DescribeUserProfileOutput, error) { input := &sagemaker.DescribeUserProfileInput{ DomainId: aws.String(domainID), @@ -940,21 +965,62 @@ func findUserProfileByName(ctx context.Context, conn *sagemaker.Client, domainID return output, nil } -func decodeUserProfileName(id string) (string, string, error) { - userProfileARN, err := arn.Parse(id) - if err != nil { - return "", "", err +func statusUserProfile(ctx context.Context, conn *sagemaker.Client, domainID, userProfileName string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := findUserProfileByName(ctx, conn, domainID, userProfileName) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, string(output.Status), nil } +} - userProfileResourceNameName := strings.TrimPrefix(userProfileARN.Resource, "user-profile/") - parts := strings.Split(userProfileResourceNameName, "/") +func waitUserProfileInService(ctx context.Context, conn *sagemaker.Client, domainID, userProfileName string) (*sagemaker.DescribeUserProfileOutput, error) { + const ( + timeout = 10 * time.Minute + ) + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.UserProfileStatusPending, awstypes.UserProfileStatusUpdating), + Target: enum.Slice(awstypes.UserProfileStatusInService), + Refresh: statusUserProfile(ctx, conn, domainID, userProfileName), + Timeout: timeout, + } - if len(parts) != 2 { - return "", "", fmt.Errorf("Unexpected format of ID (%q), expected DOMAIN-ID/USER-PROFILE-NAME", userProfileResourceNameName) + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*sagemaker.DescribeUserProfileOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString(output.FailureReason))) + + return output, err } - domainID := parts[0] - userProfileName := parts[1] + return nil, err +} - return domainID, userProfileName, nil +func waitUserProfileDeleted(ctx context.Context, conn *sagemaker.Client, domainID, userProfileName string) (*sagemaker.DescribeUserProfileOutput, error) { + const ( + timeout = 10 * time.Minute + ) + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.UserProfileStatusDeleting), + Target: []string{}, + Refresh: statusUserProfile(ctx, conn, domainID, userProfileName), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*sagemaker.DescribeUserProfileOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString(output.FailureReason))) + + return output, err + } + + return nil, err } diff --git a/internal/service/sagemaker/wait.go b/internal/service/sagemaker/wait.go index eb77f2d6df0..9865d0a4d19 100644 --- a/internal/service/sagemaker/wait.go +++ b/internal/service/sagemaker/wait.go @@ -32,8 +32,6 @@ const ( domainDeletedTimeout = 20 * time.Minute featureGroupCreatedTimeout = 20 * time.Minute featureGroupDeletedTimeout = 10 * time.Minute - userProfileInServiceTimeout = 10 * time.Minute - userProfileDeletedTimeout = 10 * time.Minute appInServiceTimeout = 10 * time.Minute appDeletedTimeout = 10 * time.Minute flowDefinitionActiveTimeout = 2 * time.Minute @@ -363,48 +361,6 @@ func waitFeatureGroupDeleted(ctx context.Context, conn *sagemaker.Client, name s return nil, err } -func waitUserProfileInService(ctx context.Context, conn *sagemaker.Client, domainID, userProfileName string) error { - stateConf := &retry.StateChangeConf{ - Pending: enum.Slice(awstypes.UserProfileStatusPending, awstypes.UserProfileStatusUpdating), - Target: enum.Slice(awstypes.UserProfileStatusInService), - Refresh: statusUserProfile(ctx, conn, domainID, userProfileName), - Timeout: userProfileInServiceTimeout, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - - if output, ok := outputRaw.(*sagemaker.DescribeUserProfileOutput); ok { - if status, reason := output.Status, aws.ToString(output.FailureReason); status == awstypes.UserProfileStatusFailed || status == awstypes.UserProfileStatusUpdateFailed && reason != "" { - tfresource.SetLastError(err, errors.New(reason)) - } - - return err - } - - return err -} - -func waitUserProfileDeleted(ctx context.Context, conn *sagemaker.Client, domainID, userProfileName string) (*sagemaker.DescribeUserProfileOutput, error) { - stateConf := &retry.StateChangeConf{ - Pending: enum.Slice(awstypes.UserProfileStatusDeleting), - Target: []string{}, - Refresh: statusUserProfile(ctx, conn, domainID, userProfileName), - Timeout: userProfileDeletedTimeout, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - - if output, ok := outputRaw.(*sagemaker.DescribeUserProfileOutput); ok { - if status, reason := output.Status, aws.ToString(output.FailureReason); status == awstypes.UserProfileStatusDeleteFailed && reason != "" { - tfresource.SetLastError(err, errors.New(reason)) - } - - return output, err - } - - return nil, err -} - func waitAppInService(ctx context.Context, conn *sagemaker.Client, domainID, userProfileOrSpaceName, appType, appName string) (*sagemaker.DescribeAppOutput, error) { stateConf := &retry.StateChangeConf{ Pending: enum.Slice(awstypes.AppStatusPending), From 8260e9b018f4b0ec2eab097bb7eccaa8d464a50b Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 9 Sep 2024 12:55:16 -0400 Subject: [PATCH 4/4] Fix golangci-lint 'unparam'. --- internal/service/sagemaker/user_profile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/sagemaker/user_profile.go b/internal/service/sagemaker/user_profile.go index 983544d2ae8..897648b9b9c 100644 --- a/internal/service/sagemaker/user_profile.go +++ b/internal/service/sagemaker/user_profile.go @@ -981,7 +981,7 @@ func statusUserProfile(ctx context.Context, conn *sagemaker.Client, domainID, us } } -func waitUserProfileInService(ctx context.Context, conn *sagemaker.Client, domainID, userProfileName string) (*sagemaker.DescribeUserProfileOutput, error) { +func waitUserProfileInService(ctx context.Context, conn *sagemaker.Client, domainID, userProfileName string) (*sagemaker.DescribeUserProfileOutput, error) { //nolint:unparam const ( timeout = 10 * time.Minute )