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_image - new resource #16082

Merged
merged 14 commits into from
Jan 4, 2021
19 changes: 19 additions & 0 deletions aws/internal/service/sagemaker/finder/finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
33 changes: 33 additions & 0 deletions aws/internal/service/sagemaker/waiter/status.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -9,6 +11,8 @@ import (

const (
SagemakerNotebookInstanceStatusNotFound = "NotFound"
SagemakerImageStatusNotFound = "NotFound"
SagemakerImageStatusFailed = "Failed"
)

// NotebookInstanceStatus fetches the NotebookInstance and its Status
Expand All @@ -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
}
}
41 changes: 41 additions & 0 deletions aws/internal/service/sagemaker/waiter/waiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
3 changes: 2 additions & 1 deletion aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
212 changes: 212 additions & 0 deletions aws/resource_aws_sagemaker_image.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading