diff --git a/aws/internal/service/sagemaker/finder/finder.go b/aws/internal/service/sagemaker/finder/finder.go index 025fd543c0c..0abb4a7a7e9 100644 --- a/aws/internal/service/sagemaker/finder/finder.go +++ b/aws/internal/service/sagemaker/finder/finder.go @@ -23,3 +23,22 @@ func CodeRepositoryByName(conn *sagemaker.SageMaker, name string) (*sagemaker.De return output, nil } + +// ImageByName returns the code repository corresponding to the specified name. +// Returns nil if no code repository is found. +func ImageByName(conn *sagemaker.SageMaker, name string) (*sagemaker.DescribeImageOutput, error) { + input := &sagemaker.DescribeImageInput{ + ImageName: aws.String(name), + } + + output, err := conn.DescribeImage(input) + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + return output, nil +} diff --git a/aws/internal/service/sagemaker/waiter/status.go b/aws/internal/service/sagemaker/waiter/status.go index 6a4943e22ab..1f8f5d00dbe 100644 --- a/aws/internal/service/sagemaker/waiter/status.go +++ b/aws/internal/service/sagemaker/waiter/status.go @@ -1,6 +1,8 @@ package waiter import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/sagemaker" "github.com/hashicorp/aws-sdk-go-base/tfawserr" @@ -9,6 +11,8 @@ import ( const ( SagemakerNotebookInstanceStatusNotFound = "NotFound" + SagemakerImageStatusNotFound = "NotFound" + SagemakerImageStatusFailed = "Failed" ) // NotebookInstanceStatus fetches the NotebookInstance and its Status @@ -35,3 +39,32 @@ func NotebookInstanceStatus(conn *sagemaker.SageMaker, notebookName string) reso return output, aws.StringValue(output.NotebookInstanceStatus), nil } } + +// ImageStatus fetches the Image and its Status +func ImageStatus(conn *sagemaker.SageMaker, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &sagemaker.DescribeImageInput{ + ImageName: aws.String(name), + } + + output, err := conn.DescribeImage(input) + + if tfawserr.ErrMessageContains(err, sagemaker.ErrCodeResourceNotFound, "No Image with the name") { + return nil, SagemakerImageStatusNotFound, nil + } + + if err != nil { + return nil, SagemakerImageStatusFailed, err + } + + if output == nil { + return nil, SagemakerImageStatusNotFound, nil + } + + if aws.StringValue(output.ImageStatus) == sagemaker.ImageStatusCreateFailed { + return output, sagemaker.ImageStatusCreateFailed, fmt.Errorf("%s", aws.StringValue(output.FailureReason)) + } + + return output, aws.StringValue(output.ImageStatus), nil + } +} diff --git a/aws/internal/service/sagemaker/waiter/waiter.go b/aws/internal/service/sagemaker/waiter/waiter.go index 61330bea240..e6ff40fb82a 100644 --- a/aws/internal/service/sagemaker/waiter/waiter.go +++ b/aws/internal/service/sagemaker/waiter/waiter.go @@ -11,6 +11,8 @@ const ( NotebookInstanceInServiceTimeout = 10 * time.Minute NotebookInstanceStoppedTimeout = 10 * time.Minute NotebookInstanceDeletedTimeout = 10 * time.Minute + ImageCreatedTimeout = 10 * time.Minute + ImageDeletedTimeout = 10 * time.Minute ) // NotebookInstanceInService waits for a NotebookInstance to return InService @@ -76,3 +78,42 @@ func NotebookInstanceDeleted(conn *sagemaker.SageMaker, notebookName string) (*s return nil, err } + +// ImageCreated waits for a Image to return Created +func ImageCreated(conn *sagemaker.SageMaker, name string) (*sagemaker.DescribeImageOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + sagemaker.ImageStatusCreating, + sagemaker.ImageStatusUpdating, + }, + Target: []string{sagemaker.ImageStatusCreated}, + Refresh: ImageStatus(conn, name), + Timeout: ImageCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*sagemaker.DescribeImageOutput); ok { + return output, err + } + + return nil, err +} + +// ImageDeleted waits for a Image to return Deleted +func ImageDeleted(conn *sagemaker.SageMaker, name string) (*sagemaker.DescribeImageOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{sagemaker.ImageStatusDeleting}, + Target: []string{}, + Refresh: ImageStatus(conn, name), + Timeout: ImageDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*sagemaker.DescribeImageOutput); ok { + return output, err + } + + return nil, err +} diff --git a/aws/provider.go b/aws/provider.go index 79d050c6a0a..5fda836eed7 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -857,9 +857,10 @@ func Provider() *schema.Provider { "aws_default_route_table": resourceAwsDefaultRouteTable(), "aws_route_table_association": resourceAwsRouteTableAssociation(), "aws_sagemaker_code_repository": resourceAwsSagemakerCodeRepository(), - "aws_sagemaker_model": resourceAwsSagemakerModel(), "aws_sagemaker_endpoint_configuration": resourceAwsSagemakerEndpointConfiguration(), + "aws_sagemaker_image": resourceAwsSagemakerImage(), "aws_sagemaker_endpoint": resourceAwsSagemakerEndpoint(), + "aws_sagemaker_model": resourceAwsSagemakerModel(), "aws_sagemaker_notebook_instance_lifecycle_configuration": resourceAwsSagemakerNotebookInstanceLifeCycleConfiguration(), "aws_sagemaker_notebook_instance": resourceAwsSagemakerNotebookInstance(), "aws_secretsmanager_secret": resourceAwsSecretsManagerSecret(), diff --git a/aws/resource_aws_sagemaker_image.go b/aws/resource_aws_sagemaker_image.go new file mode 100644 index 00000000000..3da9bdb809c --- /dev/null +++ b/aws/resource_aws_sagemaker_image.go @@ -0,0 +1,212 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sagemaker" + "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/service/sagemaker/waiter" +) + +func resourceAwsSagemakerImage() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSagemakerImageCreate, + Read: resourceAwsSagemakerImageRead, + Update: resourceAwsSagemakerImageUpdate, + Delete: resourceAwsSagemakerImageDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "image_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)."), + ), + }, + "role_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "display_name": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 128), + }, + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 512), + }, + "tags": tagsSchema(), + }, + } +} + +func resourceAwsSagemakerImageCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).sagemakerconn + + name := d.Get("image_name").(string) + input := &sagemaker.CreateImageInput{ + ImageName: aws.String(name), + RoleArn: aws.String(d.Get("role_arn").(string)), + } + + if v, ok := d.GetOk("display_name"); ok { + input.DisplayName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("tags"); ok { + input.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().SagemakerTags() + } + + // for some reason even if the operation is retried the same error response is given even though the role is valid. a short sleep before creation solves it. + time.Sleep(1 * time.Minute) + _, err := conn.CreateImage(input) + if err != nil { + return fmt.Errorf("error creating SageMaker Image %s: %w", name, err) + } + + d.SetId(name) + + if _, err := waiter.ImageCreated(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for SageMaker Image (%s) to be created: %w", d.Id(), err) + } + + return resourceAwsSagemakerImageRead(d, meta) +} + +func resourceAwsSagemakerImageRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).sagemakerconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + image, err := finder.ImageByName(conn, d.Id()) + if err != nil { + if isAWSErr(err, sagemaker.ErrCodeResourceNotFound, "No Image with the name") { + d.SetId("") + log.Printf("[WARN] Unable to find SageMaker Image (%s); removing from state", d.Id()) + return nil + } + return fmt.Errorf("error reading SageMaker Image (%s): %w", d.Id(), err) + + } + + arn := aws.StringValue(image.ImageArn) + d.Set("image_name", image.ImageName) + d.Set("arn", arn) + d.Set("role_arn", image.RoleArn) + d.Set("display_name", image.DisplayName) + d.Set("description", image.Description) + + tags, err := keyvaluetags.SagemakerListTags(conn, arn) + + if err != nil { + return fmt.Errorf("error listing tags for SageMaker Image (%s): %w", d.Id(), err) + } + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + return nil +} + +func resourceAwsSagemakerImageUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).sagemakerconn + needsUpdate := false + + input := &sagemaker.UpdateImageInput{ + ImageName: aws.String(d.Id()), + } + + var deleteProperties []*string + + if d.HasChange("description") { + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } else { + deleteProperties = append(deleteProperties, aws.String("Description")) + input.DeleteProperties = deleteProperties + } + needsUpdate = true + } + + if d.HasChange("display_name") { + if v, ok := d.GetOk("display_name"); ok { + input.DisplayName = aws.String(v.(string)) + } else { + deleteProperties = append(deleteProperties, aws.String("DisplayName")) + input.DeleteProperties = deleteProperties + } + needsUpdate = true + } + + if needsUpdate { + log.Printf("[DEBUG] sagemaker Image update config: %#v", *input) + _, err := conn.UpdateImage(input) + if err != nil { + return fmt.Errorf("error updating SageMaker Image: %w", err) + } + + if _, err := waiter.ImageCreated(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for SageMaker Image (%s) to update: %w", d.Id(), err) + } + } + + if d.HasChange("tags") { + o, n := d.GetChange("tags") + + if err := keyvaluetags.SagemakerUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return fmt.Errorf("error updating SageMaker Image (%s) tags: %s", d.Id(), err) + } + } + + return resourceAwsSagemakerImageRead(d, meta) +} + +func resourceAwsSagemakerImageDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).sagemakerconn + + input := &sagemaker.DeleteImageInput{ + ImageName: aws.String(d.Id()), + } + + if _, err := conn.DeleteImage(input); err != nil { + if isAWSErr(err, sagemaker.ErrCodeResourceNotFound, "No Image with the name") { + return nil + } + return fmt.Errorf("error deleting SageMaker Image (%s): %w", d.Id(), err) + } + + if _, err := waiter.ImageDeleted(conn, d.Id()); err != nil { + if isAWSErr(err, sagemaker.ErrCodeResourceNotFound, "No Image with the name") { + return nil + } + return fmt.Errorf("error waiting for SageMaker Image (%s) to delete: %w", d.Id(), err) + + } + + return nil +} diff --git a/aws/resource_aws_sagemaker_image_test.go b/aws/resource_aws_sagemaker_image_test.go new file mode 100644 index 00000000000..d2f25826f59 --- /dev/null +++ b/aws/resource_aws_sagemaker_image_test.go @@ -0,0 +1,355 @@ +package aws + +import ( + "fmt" + "log" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sagemaker" + "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/terraform-providers/terraform-provider-aws/aws/internal/service/sagemaker/finder" +) + +func init() { + resource.AddTestSweepers("aws_sagemaker_image", &resource.Sweeper{ + Name: "aws_sagemaker_image", + F: testSweepSagemakerImages, + }) +} + +func testSweepSagemakerImages(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.(*AWSClient).sagemakerconn + + err = conn.ListImagesPages(&sagemaker.ListImagesInput{}, func(page *sagemaker.ListImagesOutput, lastPage bool) bool { + for _, Image := range page.Images { + name := aws.StringValue(Image.ImageName) + + input := &sagemaker.DeleteImageInput{ + ImageName: Image.ImageName, + } + + log.Printf("[INFO] Deleting SageMaker Image: %s", name) + if _, err := conn.DeleteImage(input); err != nil { + log.Printf("[ERROR] Error deleting SageMaker Image (%s): %s", name, err) + continue + } + } + + return !lastPage + }) + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping SageMaker Image sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("Error retrieving SageMaker Images: %w", err) + } + + return nil +} + +func TestAccAWSSagemakerImage_basic(t *testing.T) { + var image sagemaker.DescribeImageOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_sagemaker_image.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSagemakerImageDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSagemakerImageBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerImageExists(resourceName, &image), + resource.TestCheckResourceAttr(resourceName, "image_name", rName), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "sagemaker", fmt.Sprintf("image/%s", rName)), + resource.TestCheckResourceAttrPair(resourceName, "role_arn", "aws_iam_role.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSSagemakerImage_description(t *testing.T) { + var image sagemaker.DescribeImageOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_sagemaker_image.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSagemakerImageDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSagemakerImageDescription(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerImageExists(resourceName, &image), + resource.TestCheckResourceAttr(resourceName, "description", rName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSSagemakerImageBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerImageExists(resourceName, &image), + resource.TestCheckResourceAttr(resourceName, "description", ""), + ), + }, + { + Config: testAccAWSSagemakerImageDescription(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerImageExists(resourceName, &image), + resource.TestCheckResourceAttr(resourceName, "description", rName), + ), + }, + }, + }) +} + +func TestAccAWSSagemakerImage_displayName(t *testing.T) { + var image sagemaker.DescribeImageOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_sagemaker_image.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSagemakerImageDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSagemakerImageDisplayName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerImageExists(resourceName, &image), + resource.TestCheckResourceAttr(resourceName, "display_name", rName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSSagemakerImageBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerImageExists(resourceName, &image), + resource.TestCheckResourceAttr(resourceName, "display_name", ""), + ), + }, + { + Config: testAccAWSSagemakerImageDisplayName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerImageExists(resourceName, &image), + resource.TestCheckResourceAttr(resourceName, "display_name", rName), + ), + }, + }, + }) +} + +func TestAccAWSSagemakerImage_tags(t *testing.T) { + var image sagemaker.DescribeImageOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_sagemaker_image.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSagemakerImageDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSagemakerImageConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerImageExists(resourceName, &image), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSSagemakerImageConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerImageExists(resourceName, &image), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSSagemakerImageConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerImageExists(resourceName, &image), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccAWSSagemakerImage_disappears(t *testing.T) { + var image sagemaker.DescribeImageOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_sagemaker_image.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSagemakerImageDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSagemakerImageBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerImageExists(resourceName, &image), + testAccCheckResourceDisappears(testAccProvider, resourceAwsSagemakerImage(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckAWSSagemakerImageDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).sagemakerconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_sagemaker_image" { + continue + } + + Image, err := finder.ImageByName(conn, rs.Primary.ID) + if err != nil { + return nil + } + + if aws.StringValue(Image.ImageName) == rs.Primary.ID { + return fmt.Errorf("sagemaker Image %q still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckAWSSagemakerImageExists(n string, image *sagemaker.DescribeImageOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No sagmaker Image ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).sagemakerconn + resp, err := finder.ImageByName(conn, rs.Primary.ID) + if err != nil { + return err + } + + *image = *resp + + return nil + } +} + +func testAccAWSSagemakerImageConfigBase(rName string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_iam_role" "test" { + name = %[1]q + assume_role_policy = data.aws_iam_policy_document.test.json +} + +data "aws_iam_policy_document" "test" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["sagemaker.${data.aws_partition.current.dns_suffix}"] + } + } +} +`, rName) +} + +func testAccAWSSagemakerImageBasicConfig(rName string) string { + return testAccAWSSagemakerImageConfigBase(rName) + fmt.Sprintf(` +resource "aws_sagemaker_image" "test" { + image_name = %[1]q + role_arn = aws_iam_role.test.arn +} +`, rName) +} + +func testAccAWSSagemakerImageDescription(rName string) string { + return testAccAWSSagemakerImageConfigBase(rName) + fmt.Sprintf(` +resource "aws_sagemaker_image" "test" { + image_name = %[1]q + role_arn = aws_iam_role.test.arn + description = %[1]q +} +`, rName) +} + +func testAccAWSSagemakerImageDisplayName(rName string) string { + return testAccAWSSagemakerImageConfigBase(rName) + fmt.Sprintf(` +resource "aws_sagemaker_image" "test" { + image_name = %[1]q + role_arn = aws_iam_role.test.arn + display_name = %[1]q +} +`, rName) +} + +func testAccAWSSagemakerImageConfigTags1(rName, tagKey1, tagValue1 string) string { + return testAccAWSSagemakerImageConfigBase(rName) + fmt.Sprintf(` +resource "aws_sagemaker_image" "test" { + image_name = %[1]q + role_arn = aws_iam_role.test.arn + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccAWSSagemakerImageConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return testAccAWSSagemakerImageConfigBase(rName) + fmt.Sprintf(` +resource "aws_sagemaker_image" "test" { + image_name = %[1]q + role_arn = aws_iam_role.test.arn + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/website/docs/r/sagemaker_image.html.markdown b/website/docs/r/sagemaker_image.html.markdown new file mode 100644 index 00000000000..ebe5416cee1 --- /dev/null +++ b/website/docs/r/sagemaker_image.html.markdown @@ -0,0 +1,47 @@ +--- +subcategory: "Sagemaker" +layout: "aws" +page_title: "AWS: aws_sagemaker_image" +description: |- + Provides a Sagemaker Image resource. +--- + +# Resource: aws_sagemaker_image + +Provides a Sagemaker Image resource. + +## Example Usage + +### Basic usage + +```hcl +resource "aws_sagemaker_image" "example" { + image_name = "example" + role_arn = aws_iam_role.test.arn +} +``` + +## Argument Reference + +The following arguments are supported: + +* `image_name` - (Required) The name of the image. Must be unique to your account. +* `role_arn` - (Required) The Amazon Resource Name (ARN) of an IAM role that enables Amazon SageMaker to perform tasks on your behalf. +* `display_name` - (Optional) The display name of the image. When the image is added to a domain (must be unique to the domain). +* `description` - (Optional) The description of the image. +* `tags` - (Optional) A map of tags to assign to the resource. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The name of the Image. +* `arn` - The Amazon Resource Name (ARN) assigned by AWS to this Image. + +## Import + +Sagemaker Code Images can be imported using the `name`, e.g. + +``` +$ terraform import aws_sagemaker_image.test_image my-code-repo +```