Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

r/sagemaker_studio_lifecycle_config - new resource + usage in domain and user profile #21041

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changelog/21041.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
```release-note:new-resource
aws_sagemaker_studio_lifecycle_config
```

```release-note:enhancement
resource/aws_sagemaker_domain: Add `default_user_settings.jupyter_server_app_settings.lifecycle_config_arns` and `default_user_settings.kernel_gateway_app_settings.lifecycle_config_arns` arguments
```

```release-note:enhancement
resource/aws_user_profile: Add `user_settings.jupyter_server_app_settings.lifecycle_config_arns` and `user_settings.kernel_gateway_app_settings.lifecycle_config_arns` arguments
```
25 changes: 25 additions & 0 deletions aws/internal/service/sagemaker/finder/finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,28 @@ func FlowDefinitionByName(conn *sagemaker.SageMaker, name string) (*sagemaker.De

return output, nil
}

func StudioLifecycleConfigByName(conn *sagemaker.SageMaker, name string) (*sagemaker.DescribeStudioLifecycleConfigOutput, error) {
input := &sagemaker.DescribeStudioLifecycleConfigInput{
StudioLifecycleConfigName: aws.String(name),
}

output, err := conn.DescribeStudioLifecycleConfig(input)

if tfawserr.ErrCodeEquals(err, sagemaker.ErrCodeResourceNotFound) {
return nil, &resource.NotFoundError{
LastError: err,
LastRequest: input,
}
}

if err != nil {
return nil, err
}

if output == nil {
return nil, tfresource.NewEmptyResultError(input)
}

return output, nil
}
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,7 @@ func Provider() *schema.Provider {
"aws_sagemaker_model_package_group": resourceAwsSagemakerModelPackageGroup(),
"aws_sagemaker_notebook_instance_lifecycle_configuration": resourceAwsSagemakerNotebookInstanceLifeCycleConfiguration(),
"aws_sagemaker_notebook_instance": resourceAwsSagemakerNotebookInstance(),
"aws_sagemaker_studio_lifecycle_config": resourceAwsSagemakerStudioLifecycleConfig(),
"aws_sagemaker_user_profile": resourceAwsSagemakerUserProfile(),
"aws_sagemaker_workforce": resourceAwsSagemakerWorkforce(),
"aws_sagemaker_workteam": resourceAwsSagemakerWorkteam(),
Expand Down
32 changes: 32 additions & 0 deletions aws/resource_aws_sagemaker_domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ func resourceAwsSagemakerDomain() *schema.Resource {
},
},
},
"lifecycle_config_arns": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validateArn,
},
},
},
},
},
Expand Down Expand Up @@ -197,6 +205,14 @@ func resourceAwsSagemakerDomain() *schema.Resource {
},
},
},
"lifecycle_config_arns": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validateArn,
},
},
"custom_image": {
Type: schema.TypeList,
Optional: true,
Expand Down Expand Up @@ -478,6 +494,10 @@ func expandSagemakerDomainJupyterServerAppSettings(l []interface{}) *sagemaker.J
config.DefaultResourceSpec = expandSagemakerDomainDefaultResourceSpec(v)
}

if v, ok := m["lifecycle_config_arns"].(*schema.Set); ok && v.Len() > 0 {
config.LifecycleConfigArns = expandStringSet(v)
}

return config
}

Expand All @@ -494,6 +514,10 @@ func expandSagemakerDomainKernelGatewayAppSettings(l []interface{}) *sagemaker.K
config.DefaultResourceSpec = expandSagemakerDomainDefaultResourceSpec(v)
}

if v, ok := m["lifecycle_config_arns"].(*schema.Set); ok && v.Len() > 0 {
config.LifecycleConfigArns = expandStringSet(v)
}

if v, ok := m["custom_image"].([]interface{}); ok && len(v) > 0 {
config.CustomImages = expandSagemakerDomainCustomImages(v)
}
Expand Down Expand Up @@ -657,6 +681,10 @@ func flattenSagemakerDomainJupyterServerAppSettings(config *sagemaker.JupyterSer
m["default_resource_spec"] = flattenSagemakerDomainDefaultResourceSpec(config.DefaultResourceSpec)
}

if config.LifecycleConfigArns != nil {
m["lifecycle_config_arns"] = flattenStringSet(config.LifecycleConfigArns)
}

return []map[string]interface{}{m}
}

Expand All @@ -671,6 +699,10 @@ func flattenSagemakerDomainKernelGatewayAppSettings(config *sagemaker.KernelGate
m["default_resource_spec"] = flattenSagemakerDomainDefaultResourceSpec(config.DefaultResourceSpec)
}

if config.LifecycleConfigArns != nil {
m["lifecycle_config_arns"] = flattenStringSet(config.LifecycleConfigArns)
}

if config.CustomImages != nil {
m["custom_image"] = flattenSagemakerDomainCustomImages(config.CustomImages)
}
Expand Down
65 changes: 65 additions & 0 deletions aws/resource_aws_sagemaker_domain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,38 @@ func testAccAWSSagemakerDomain_kernelGatewayAppSettings(t *testing.T) {
})
}

func testAccAWSSagemakerDomain_kernelGatewayAppSettings_lifecycleConfig(t *testing.T) {
var domain sagemaker.DescribeDomainOutput
rName := acctest.RandomWithPrefix("tf-acc-test")
resourceName := "aws_sagemaker_domain.test"

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ErrorCheck: testAccErrorCheck(t, sagemaker.EndpointsID),
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSagemakerDomainDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSSagemakerDomainConfigKernelGatewayAppSettingsLifecycleConfig(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSagemakerDomainExists(resourceName, &domain),
resource.TestCheckResourceAttr(resourceName, "default_user_settings.#", "1"),
resource.TestCheckResourceAttr(resourceName, "default_user_settings.0.kernel_gateway_app_settings.#", "1"),
resource.TestCheckResourceAttr(resourceName, "default_user_settings.0.kernel_gateway_app_settings.0.lifecycle_config_arns.#", "1"),
resource.TestCheckResourceAttr(resourceName, "default_user_settings.0.kernel_gateway_app_settings.0.default_resource_spec.#", "1"),
resource.TestCheckResourceAttr(resourceName, "default_user_settings.0.kernel_gateway_app_settings.0.default_resource_spec.0.instance_type", "ml.t3.micro"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"retention_policy"},
},
},
})
}

func testAccAWSSagemakerDomain_kernelGatewayAppSettings_customImage(t *testing.T) {

if os.Getenv("SAGEMAKER_IMAGE_VERSION_BASE_IMAGE") == "" {
Expand Down Expand Up @@ -820,6 +852,39 @@ resource "aws_sagemaker_domain" "test" {
`, rName)
}

func testAccAWSSagemakerDomainConfigKernelGatewayAppSettingsLifecycleConfig(rName string) string {
return testAccAWSSagemakerDomainConfigBase(rName) + fmt.Sprintf(`
resource "aws_sagemaker_studio_lifecycle_config" "test" {
studio_lifecycle_config_name = %[1]q
studio_lifecycle_config_app_type = "JupyterServer"
studio_lifecycle_config_content = base64encode("echo Hello")
}

resource "aws_sagemaker_domain" "test" {
domain_name = %[1]q
auth_mode = "IAM"
vpc_id = aws_vpc.test.id
subnet_ids = [aws_subnet.test.id]

default_user_settings {
execution_role = aws_iam_role.test.arn

kernel_gateway_app_settings {
default_resource_spec {
instance_type = "ml.t3.micro"
}

lifecycle_config_arns = [aws_sagemaker_studio_lifecycle_config.test.arn]
}
}

retention_policy {
home_efs_file_system = "Delete"
}
}
`, rName)
}

func testAccAWSSagemakerDomainConfigKernelGatewayAppSettingsCustomImage(rName, baseImage string) string {
return testAccAWSSagemakerDomainConfigBase(rName) + fmt.Sprintf(`
resource "aws_sagemaker_image" "test" {
Expand Down
164 changes: 164 additions & 0 deletions aws/resource_aws_sagemaker_studio_lifecycle_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package aws

import (
"fmt"
"log"
"regexp"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/sagemaker"
"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/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/sagemaker/finder"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

func resourceAwsSagemakerStudioLifecycleConfig() *schema.Resource {
return &schema.Resource{
Create: resourceAwsSagemakerStudioLifecycleConfigCreate,
Read: resourceAwsSagemakerStudioLifecycleConfigRead,
Update: resourceAwsSagemakerStudioLifecycleConfigUpdate,
Delete: resourceAwsSagemakerStudioLifecycleConfigDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"arn": {
Type: schema.TypeString,
Computed: true,
},
"studio_lifecycle_config_app_type": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice(sagemaker.StudioLifecycleConfigAppType_Values(), false),
},
"studio_lifecycle_config_content": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringLenBetween(1, 16384),
},
"studio_lifecycle_config_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.All(
validation.StringLenBetween(1, 63),
validation.StringMatch(regexp.MustCompile(`^[a-zA-Z0-9](-*[a-zA-Z0-9])*$`), "Valid characters are a-z, A-Z, 0-9, and - (hyphen)."),
),
},
"tags": tagsSchema(),
"tags_all": tagsSchemaComputed(),
},

CustomizeDiff: SetTagsDiff,
}
}

func resourceAwsSagemakerStudioLifecycleConfigCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).sagemakerconn
defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig
tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{})))

name := d.Get("studio_lifecycle_config_name").(string)
input := &sagemaker.CreateStudioLifecycleConfigInput{
StudioLifecycleConfigName: aws.String(name),
StudioLifecycleConfigAppType: aws.String(d.Get("studio_lifecycle_config_app_type").(string)),
StudioLifecycleConfigContent: aws.String(d.Get("studio_lifecycle_config_content").(string)),
}

if len(tags) > 0 {
input.Tags = tags.IgnoreAws().SagemakerTags()
}

log.Printf("[DEBUG] Creating SageMaker Studio Lifecycle Config : %s", input)
_, err := conn.CreateStudioLifecycleConfig(input)

if err != nil {
return fmt.Errorf("error creating SageMaker Studio Lifecycle Config (%s): %w", name, err)
}

d.SetId(name)

return resourceAwsSagemakerStudioLifecycleConfigRead(d, meta)
}

func resourceAwsSagemakerStudioLifecycleConfigRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).sagemakerconn
defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig
ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig

image, err := finder.StudioLifecycleConfigByName(conn, d.Id())

if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] SageMaker Studio Lifecycle Config (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}

if err != nil {
return fmt.Errorf("error reading SageMaker Studio Lifecycle Config (%s): %w", d.Id(), err)
}

arn := aws.StringValue(image.StudioLifecycleConfigArn)
d.Set("studio_lifecycle_config_name", image.StudioLifecycleConfigName)
d.Set("studio_lifecycle_config_app_type", image.StudioLifecycleConfigAppType)
d.Set("studio_lifecycle_config_content", image.StudioLifecycleConfigContent)
d.Set("arn", arn)

tags, err := keyvaluetags.SagemakerListTags(conn, arn)

if err != nil {
return fmt.Errorf("error listing tags for SageMaker Studio Lifecycle Config (%s): %w", d.Id(), err)
}

tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig)

//lintignore:AWSR002
if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil {
return fmt.Errorf("error setting tags: %w", err)
}

if err := d.Set("tags_all", tags.Map()); err != nil {
return fmt.Errorf("error setting tags_all: %w", err)
}

return nil
}

func resourceAwsSagemakerStudioLifecycleConfigUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).sagemakerconn

if d.HasChange("tags_all") {
o, n := d.GetChange("tags_all")

if err := keyvaluetags.SagemakerUpdateTags(conn, d.Get("arn").(string), o, n); err != nil {
return fmt.Errorf("error updating Studio Lifecycle Config (%s) tags: %w", d.Id(), err)
}
}

return resourceAwsSagemakerStudioLifecycleConfigRead(d, meta)
}

func resourceAwsSagemakerStudioLifecycleConfigDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).sagemakerconn

input := &sagemaker.DeleteStudioLifecycleConfigInput{
StudioLifecycleConfigName: aws.String(d.Id()),
}

log.Printf("[DEBUG] Deleting SageMaker Studio Lifecycle Config: (%s)", d.Id())
if _, err := conn.DeleteStudioLifecycleConfig(input); err != nil {
if tfawserr.ErrMessageContains(err, sagemaker.ErrCodeResourceNotFound, "does not exist") {
return nil
}

return fmt.Errorf("error deleting SageMaker Studio Lifecycle Config (%s): %w", d.Id(), err)
}

return nil
}
Loading